From 45c9aaa5912bce5b6910c8fddeb834bafceccae8 Mon Sep 17 00:00:00 2001 From: ssongliu Date: Tue, 23 Jul 2024 14:48:05 +0800 Subject: [PATCH] =?UTF-8?q?pref:=20=E4=BF=AE=E6=94=B9=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/v1/app.go | 197 - backend/app/api/v1/app_install.go | 327 - backend/app/api/v1/backup.go | 443 - backend/app/api/v1/clam.go | 296 - backend/app/api/v1/command.go | 223 - backend/app/api/v1/compose_template.go | 121 - backend/app/api/v1/container.go | 699 - backend/app/api/v1/cronjob.go | 241 - backend/app/api/v1/dashboard.go | 99 - backend/app/api/v1/database.go | 229 - backend/app/api/v1/database_common.go | 75 - backend/app/api/v1/database_mysql.go | 350 - backend/app/api/v1/database_postgresql.go | 233 - backend/app/api/v1/database_redis.go | 170 - backend/app/api/v1/device.go | 236 - backend/app/api/v1/docker.go | 169 - backend/app/api/v1/entry.go | 69 - backend/app/api/v1/fail2ban.go | 153 - backend/app/api/v1/favorite.go | 76 - backend/app/api/v1/file.go | 801 - backend/app/api/v1/firewall.go | 224 - backend/app/api/v1/ftp.go | 199 - backend/app/api/v1/group.go | 97 - backend/app/api/v1/helper/helper.go | 140 - backend/app/api/v1/host.go | 230 - backend/app/api/v1/host_tool.go | 180 - backend/app/api/v1/image.go | 231 - backend/app/api/v1/image_repo.go | 143 - backend/app/api/v1/logs.go | 45 - backend/app/api/v1/monitor.go | 134 - backend/app/api/v1/nginx.go | 118 - backend/app/api/v1/php_extensions.go | 103 - backend/app/api/v1/process.go | 39 - backend/app/api/v1/recycle_bin.go | 86 - backend/app/api/v1/runtime.go | 235 - backend/app/api/v1/setting.go | 66 - backend/app/api/v1/snapshot.go | 187 - backend/app/api/v1/ssh.go | 169 - backend/app/api/v1/terminal.go | 285 - backend/app/api/v1/website.go | 822 - backend/app/api/v1/website_acme_account.go | 76 - backend/app/api/v1/website_ca.go | 177 - backend/app/api/v1/website_dns_account.go | 96 - backend/app/api/v1/website_domain.go | 73 - backend/app/api/v1/website_ssl.go | 248 - backend/app/dto/app.go | 149 - backend/app/dto/backup.go | 82 - backend/app/dto/clam.go | 96 - backend/app/dto/command.go | 38 - backend/app/dto/common_req.go | 54 - backend/app/dto/common_res.go | 16 - backend/app/dto/compose_template.go | 23 - backend/app/dto/container.go | 234 - backend/app/dto/cronjob.go | 121 - backend/app/dto/dashboard.go | 107 - backend/app/dto/database.go | 321 - backend/app/dto/database_postgresql.go | 85 - backend/app/dto/device.go | 41 - backend/app/dto/docker.go | 39 - backend/app/dto/fail2ban.go | 29 - backend/app/dto/firewall.go | 74 - backend/app/dto/ftp.go | 43 - backend/app/dto/group.go | 25 - backend/app/dto/host.go | 75 - backend/app/dto/image.go | 44 - backend/app/dto/image_repo.go | 44 - backend/app/dto/logs.go | 50 - backend/app/dto/monitor.go | 16 - backend/app/dto/nginx.go | 59 - backend/app/dto/request/app.go | 104 - backend/app/dto/request/favorite.go | 9 - backend/app/dto/request/file.go | 136 - backend/app/dto/request/host_tool.go | 41 - backend/app/dto/request/nginx.go | 87 - backend/app/dto/request/php_extensions.go | 22 - backend/app/dto/request/process.go | 5 - backend/app/dto/request/recycle_bin.go | 11 - backend/app/dto/request/runtime.go | 72 - backend/app/dto/request/website.go | 220 - backend/app/dto/request/website_ssl.go | 139 - backend/app/dto/response/app.go | 154 - backend/app/dto/response/favorite.go | 7 - backend/app/dto/response/file.go | 47 - backend/app/dto/response/host_tool.go | 41 - backend/app/dto/response/nginx.go | 55 - backend/app/dto/response/php_extensions.go | 7 - backend/app/dto/response/recycle_bin.go | 14 - backend/app/dto/response/runtime.go | 58 - backend/app/dto/response/website.go | 88 - backend/app/dto/response/website_ssl.go | 34 - backend/app/dto/setting.go | 124 - backend/app/dto/ssh.go | 57 - backend/app/model/app.go | 43 - backend/app/model/app_detail.go | 16 - backend/app/model/app_install.go | 47 - backend/app/model/app_install_resource.go | 10 - backend/app/model/app_tag.go | 7 - backend/app/model/backup.go | 24 - backend/app/model/base.go | 9 - backend/app/model/clam.go | 14 - backend/app/model/command.go | 14 - backend/app/model/compose_template.go | 15 - backend/app/model/cronjob.go | 50 - backend/app/model/database.go | 22 - backend/app/model/database_mysql.go | 14 - backend/app/model/database_postgresql.go | 14 - backend/app/model/favorite.go | 10 - backend/app/model/firewall.go | 21 - backend/app/model/ftp.go | 11 - backend/app/model/group.go | 8 - backend/app/model/host.go | 18 - backend/app/model/image_repo.go | 15 - backend/app/model/monitor.go | 29 - backend/app/model/php_extensions.go | 7 - backend/app/model/runtime.go | 40 - backend/app/model/setting.go | 8 - backend/app/model/snapshot.go | 35 - backend/app/model/tag.go | 8 - backend/app/model/website.go | 40 - backend/app/model/website_acme_account.go | 16 - backend/app/model/website_ca.go | 9 - backend/app/model/website_dns_account.go | 12 - backend/app/model/website_domain.go | 12 - backend/app/model/website_ssl.go | 50 - backend/app/repo/app.go | 123 - backend/app/repo/app_detail.go | 82 - backend/app/repo/app_install.go | 241 - backend/app/repo/app_install_resource.go | 82 - backend/app/repo/app_tag.go | 49 - backend/app/repo/backup.go | 135 - backend/app/repo/clam.go | 69 - backend/app/repo/command.go | 141 - backend/app/repo/common.go | 149 - backend/app/repo/compose_template.go | 104 - backend/app/repo/cronjob.go | 192 - backend/app/repo/database.go | 143 - backend/app/repo/database_mysql.go | 123 - backend/app/repo/database_postgresql.go | 123 - backend/app/repo/favorite.go | 66 - backend/app/repo/ftp.go | 80 - backend/app/repo/group.go | 69 - backend/app/repo/host.go | 159 - backend/app/repo/image_repo.go | 71 - backend/app/repo/php_extensions.go | 59 - backend/app/repo/runtime.go | 107 - backend/app/repo/setting.go | 87 - backend/app/repo/snapshot.go | 105 - backend/app/repo/tag.go | 62 - backend/app/repo/website.go | 145 - backend/app/repo/website_acme_account.go | 64 - backend/app/repo/website_ca.go | 54 - backend/app/repo/website_dns_account.go | 60 - backend/app/repo/website_domain.go | 98 - backend/app/repo/website_ssl.go | 99 - backend/app/service/app.go | 1028 - backend/app/service/app_install.go | 848 - backend/app/service/app_utils.go | 1521 - backend/app/service/backup.go | 637 - backend/app/service/backup_app.go | 315 - backend/app/service/backup_mysql.go | 193 - backend/app/service/backup_postgresql.go | 178 - backend/app/service/backup_redis.go | 194 - backend/app/service/backup_runtime.go | 129 - backend/app/service/backup_website.go | 269 - backend/app/service/clam.go | 562 - backend/app/service/command.go | 183 - backend/app/service/compose_template.go | 80 - backend/app/service/container.go | 1312 - backend/app/service/container_compose.go | 282 - backend/app/service/container_network.go | 179 - backend/app/service/container_volume.go | 143 - backend/app/service/cronjob.go | 344 - backend/app/service/cronjob_backup.go | 386 - backend/app/service/cronjob_helper.go | 393 - backend/app/service/dashboard.go | 325 - backend/app/service/database.go | 330 - backend/app/service/database_common.go | 94 - backend/app/service/database_mysql.go | 662 - backend/app/service/database_postgresql.go | 432 - backend/app/service/database_redis.go | 294 - backend/app/service/device.go | 431 - backend/app/service/device_clean.go | 735 - backend/app/service/docker.go | 388 - backend/app/service/entry.go | 46 - backend/app/service/fail2ban.go | 251 - backend/app/service/favorite.go | 83 - backend/app/service/file.go | 484 - backend/app/service/firewall.go | 694 - backend/app/service/ftp.go | 245 - backend/app/service/group.go | 90 - backend/app/service/helper.go | 14 - backend/app/service/host.go | 325 - backend/app/service/host_tool.go | 565 - backend/app/service/image.go | 517 - backend/app/service/image_repo.go | 243 - backend/app/service/logs.go | 81 - backend/app/service/monitor.go | 216 - backend/app/service/nginx.go | 146 - backend/app/service/nginx_utils.go | 231 - backend/app/service/php_extensions.go | 86 - backend/app/service/process.go | 27 - backend/app/service/recycle_bin.go | 212 - backend/app/service/runtime.go | 618 - backend/app/service/runtime_utils.go | 436 - backend/app/service/setting.go | 86 - backend/app/service/snapshot.go | 556 - backend/app/service/snapshot_create.go | 280 - backend/app/service/snapshot_recover.go | 265 - backend/app/service/ssh.go | 573 - backend/app/service/website.go | 2565 -- backend/app/service/website_acme_account.go | 78 - backend/app/service/website_ca.go | 446 - backend/app/service/website_dns_account.go | 93 - backend/app/service/website_ssl.go | 624 - backend/app/service/website_utils.go | 1117 - backend/app/task/task.go | 115 - backend/buserr/errors.go | 93 - backend/buserr/multi_err.go | 23 - backend/configs/config.go | 6 - backend/configs/log.go | 9 - backend/configs/system.go | 20 - backend/constant/app.go | 54 - backend/constant/backup.go | 18 - backend/constant/common.go | 25 - backend/constant/container.go | 18 - backend/constant/dir.go | 21 - backend/constant/errs.go | 164 - backend/constant/host_tool.go | 8 - backend/constant/nginx.go | 15 - backend/constant/runtime.go | 35 - backend/constant/session.go | 13 - backend/constant/status.go | 16 - backend/constant/website.go | 51 - backend/cron/cron.go | 102 - backend/cron/job/app.go | 20 - backend/cron/job/cache.go | 26 - backend/cron/job/ssl.go | 76 - backend/cron/job/website.go | 62 - backend/global/global.go | 30 - backend/i18n/i18n.go | 130 - backend/i18n/lang/en.yaml | 207 - backend/i18n/lang/zh-Hant.yaml | 209 - backend/i18n/lang/zh.yaml | 210 - backend/init/app/app.go | 46 - backend/init/business/business.go | 40 - backend/init/cache/badger_db/badger_db.go | 79 - backend/init/cache/cache.go | 56 - backend/init/db/db.go | 94 - backend/init/hook/hook.go | 147 - backend/init/log/log.go | 68 - backend/init/migration/migrate.go | 26 - backend/init/migration/migrations/init.go | 249 - backend/init/router/router.go | 41 - backend/init/validator/validator.go | 65 - backend/init/viper/viper.go | 90 - backend/log/config.go | 41 - backend/log/dup_write_darwin.go | 20 - backend/log/dup_write_linux.go | 20 - backend/log/dup_write_windows.go | 9 - backend/log/manager.go | 53 - backend/log/writer.go | 250 - backend/middleware/demo_handle.go | 57 - backend/middleware/helper.go | 32 - backend/middleware/loading.go | 24 - backend/router/common.go | 26 - backend/router/entry.go | 9 - backend/router/ro_app.go | 41 - backend/router/ro_container.go | 84 - backend/router/ro_cronjob.go | 26 - backend/router/ro_dashboard.go | 20 - backend/router/ro_database.go | 64 - backend/router/ro_file.go | 51 - backend/router/ro_group.go | 21 - backend/router/ro_host.go | 70 - backend/router/ro_log.go | 18 - backend/router/ro_nginx.go | 23 - backend/router/ro_process.go | 18 - backend/router/ro_router.go | 7 - backend/router/ro_runtime.go | 35 - backend/router/ro_setting.go | 45 - backend/router/ro_terminal.go | 17 - backend/router/ro_toolbox.go | 59 - backend/router/ro_website.go | 69 - backend/router/ro_website_acme_account.go | 20 - backend/router/ro_website_ca.go | 24 - backend/router/ro_website_dns_account.go | 21 - backend/router/ro_website_ssl.go | 27 - backend/server/init.go | 6 - backend/server/server.go | 55 - backend/utils/cloud_storage/client/cos.go | 142 - backend/utils/cloud_storage/client/helper.go | 18 - backend/utils/cloud_storage/client/kodo.go | 122 - backend/utils/cloud_storage/client/local.go | 97 - backend/utils/cloud_storage/client/minio.go | 149 - .../utils/cloud_storage/client/onedrive.go | 405 - backend/utils/cloud_storage/client/oss.go | 118 - backend/utils/cloud_storage/client/s3.go | 163 - backend/utils/cloud_storage/client/sftp.go | 206 - backend/utils/cloud_storage/client/webdav.go | 121 - .../cloud_storage/cloud_storage_client.go | 42 - backend/utils/cmd/cmd.go | 229 - backend/utils/common/common.go | 327 - backend/utils/compose/compose.go | 40 - backend/utils/copier/copier.go | 18 - backend/utils/docker/compose.go | 87 - backend/utils/docker/docker.go | 177 - backend/utils/encrypt/encrypt.go | 104 - backend/utils/env/env.go | 52 - backend/utils/files/archiver.go | 38 - backend/utils/files/file_op.go | 666 - backend/utils/files/fileinfo.go | 425 - backend/utils/files/tar.go | 38 - backend/utils/files/tar_gz.go | 61 - backend/utils/files/utils.go | 164 - backend/utils/files/zip.go | 52 - backend/utils/firewall/client.go | 39 - backend/utils/firewall/client/firewalld.go | 263 - backend/utils/firewall/client/info.go | 35 - backend/utils/firewall/client/iptables.go | 145 - backend/utils/firewall/client/ufw.go | 322 - backend/utils/http/get.go | 42 - backend/utils/http/request.go | 45 - backend/utils/ini_conf/ini.go | 19 - backend/utils/jwt/jwt.go | 69 - backend/utils/mysql/client.go | 77 - backend/utils/mysql/client/info.go | 137 - backend/utils/mysql/client/local.go | 381 - backend/utils/mysql/client/remote.go | 472 - backend/utils/mysql/helper/dump.go | 303 - backend/utils/mysql/helper/source.go | 244 - backend/utils/nginx/components/block.go | 88 - backend/utils/nginx/components/comment.go | 26 - backend/utils/nginx/components/config.go | 45 - backend/utils/nginx/components/directive.go | 29 - backend/utils/nginx/components/http.go | 129 - backend/utils/nginx/components/location.go | 244 - backend/utils/nginx/components/lua_block.go | 120 - backend/utils/nginx/components/server.go | 371 - .../utils/nginx/components/server_listen.go | 74 - backend/utils/nginx/components/statement.go | 19 - backend/utils/nginx/components/upstream.go | 135 - .../utils/nginx/components/upstream_server.go | 83 - backend/utils/nginx/dumper.go | 115 - backend/utils/nginx/parser/flag/flag.go | 59 - backend/utils/nginx/parser/lexer.go | 261 - backend/utils/nginx/parser/parser.go | 209 - backend/utils/ntp/ntp.go | 84 - backend/utils/postgresql/client.go | 59 - backend/utils/postgresql/client/info.go | 81 - backend/utils/postgresql/client/local.go | 230 - backend/utils/postgresql/client/remote.go | 311 - backend/utils/qqwry/qqwry.go | 165 - backend/utils/redis/redis.go | 28 - backend/utils/ssh/ssh.go | 142 - backend/utils/ssl/acme.go | 205 - backend/utils/ssl/client.go | 302 - backend/utils/systemctl/systemctl.go | 64 - backend/utils/terminal/local_cmd.go | 93 - backend/utils/terminal/ws_local_session.go | 122 - backend/utils/terminal/ws_session.go | 218 - backend/utils/toolbox/fail2ban.go | 181 - backend/utils/toolbox/pure-ftpd.go | 272 - backend/utils/websocket/client.go | 45 - backend/utils/websocket/process_data.go | 386 - backend/utils/xpack/xpack.go | 37 - cmd/server/app/app_config.go | 14 - cmd/server/app/app_config.yml | 13 - cmd/server/app/app_param.yml | 10 - cmd/server/app/logo.png | Bin 9330 -> 0 bytes {core/cmd => cmd}/server/cmd/listen-ip.go | 0 {core/cmd => cmd}/server/cmd/reset.go | 0 {core/cmd => cmd}/server/cmd/restore.go | 0 {core/cmd => cmd}/server/cmd/root.go | 0 {core/cmd => cmd}/server/cmd/update.go | 0 {core/cmd => cmd}/server/cmd/user-info.go | 0 {core/cmd => cmd}/server/cmd/version.go | 2 +- cmd/server/conf/app.yaml | 3 +- cmd/server/{ => conf}/qqwry/qqwey.go | 0 cmd/server/{ => conf}/qqwry/qqwry.dat | Bin cmd/server/main.go | 10 +- cmd/server/nginx_conf/404.html | 6 - cmd/server/nginx_conf/cache.conf | 12 - cmd/server/nginx_conf/domain404.html | 6 - cmd/server/nginx_conf/gzip.conf | 4 - cmd/server/nginx_conf/index.html | 35 - cmd/server/nginx_conf/index.php | 25 - cmd/server/nginx_conf/nginx_conf.go | 39 - cmd/server/nginx_conf/proxy.conf | 13 - cmd/server/nginx_conf/proxy_cache.conf | 4 - cmd/server/nginx_conf/proxy_no_cache.conf | 10 - cmd/server/nginx_conf/rewrite/crmeb.conf | 6 - cmd/server/nginx_conf/rewrite/dabr.conf | 5 - cmd/server/nginx_conf/rewrite/dbshop.conf | 7 - cmd/server/nginx_conf/rewrite/dedecms.conf | 10 - cmd/server/nginx_conf/rewrite/default.conf | 0 cmd/server/nginx_conf/rewrite/discuz.conf | 7 - cmd/server/nginx_conf/rewrite/discuzx.conf | 12 - cmd/server/nginx_conf/rewrite/discuzx2.conf | 14 - cmd/server/nginx_conf/rewrite/discuzx3.conf | 15 - cmd/server/nginx_conf/rewrite/drupal.conf | 3 - cmd/server/nginx_conf/rewrite/ecshop.conf | 32 - cmd/server/nginx_conf/rewrite/edusoho.conf | 8 - cmd/server/nginx_conf/rewrite/emlog.conf | 7 - cmd/server/nginx_conf/rewrite/empirecms.conf | 8 - cmd/server/nginx_conf/rewrite/laravel5.conf | 3 - cmd/server/nginx_conf/rewrite/maccms.conf | 13 - cmd/server/nginx_conf/rewrite/mvc.conf | 6 - cmd/server/nginx_conf/rewrite/niushop.conf | 6 - cmd/server/nginx_conf/rewrite/phpcms.conf | 9 - cmd/server/nginx_conf/rewrite/phpwind.conf | 4 - cmd/server/nginx_conf/rewrite/sablog.conf | 16 - cmd/server/nginx_conf/rewrite/seacms.conf | 11 - cmd/server/nginx_conf/rewrite/shopex.conf | 5 - cmd/server/nginx_conf/rewrite/shopwind.conf | 14 - cmd/server/nginx_conf/rewrite/thinkphp.conf | 8 - cmd/server/nginx_conf/rewrite/typecho.conf | 3 - cmd/server/nginx_conf/rewrite/typecho2.conf | 5 - cmd/server/nginx_conf/rewrite/wordpress.conf | 6 - cmd/server/nginx_conf/rewrite/wp2.conf | 6 - cmd/server/nginx_conf/rewrite/yii2.conf | 3 - cmd/server/nginx_conf/rewrite/zblog.conf | 9 - cmd/server/nginx_conf/ssl.conf | 9 - cmd/server/nginx_conf/stop.html | 33 - cmd/server/nginx_conf/website_default.conf | 23 - {core/cmd => cmd}/server/web/favicon.png | Bin {core/cmd => cmd}/server/web/web.go | 0 core/cmd/server/conf/app.yaml | 18 - core/cmd/server/conf/conf.go | 6 - core/cmd/server/conf/qqwry/qqwey.go | 6 - core/cmd/server/conf/qqwry/qqwry.dat | Bin 11407867 -> 0 bytes core/cmd/server/docs/docs.go | 23464 ---------------- core/cmd/server/docs/swagger.go | 6 - core/cmd/server/docs/swagger.json | 23441 --------------- core/cmd/server/docs/swagger.yaml | 15166 ---------- core/cmd/server/main.go | 28 - core/go.mod | 105 - core/go.sum | 393 - core/init/router/router.go | 4 +- core/init/viper/viper.go | 2 +- core/middleware/operation.go | 2 +- go.mod | 276 +- go.sum | 1098 +- 442 files changed, 185 insertions(+), 122143 deletions(-) delete mode 100644 backend/app/api/v1/app.go delete mode 100644 backend/app/api/v1/app_install.go delete mode 100644 backend/app/api/v1/backup.go delete mode 100644 backend/app/api/v1/clam.go delete mode 100644 backend/app/api/v1/command.go delete mode 100644 backend/app/api/v1/compose_template.go delete mode 100644 backend/app/api/v1/container.go delete mode 100644 backend/app/api/v1/cronjob.go delete mode 100644 backend/app/api/v1/dashboard.go delete mode 100644 backend/app/api/v1/database.go delete mode 100644 backend/app/api/v1/database_common.go delete mode 100644 backend/app/api/v1/database_mysql.go delete mode 100644 backend/app/api/v1/database_postgresql.go delete mode 100644 backend/app/api/v1/database_redis.go delete mode 100644 backend/app/api/v1/device.go delete mode 100644 backend/app/api/v1/docker.go delete mode 100644 backend/app/api/v1/entry.go delete mode 100644 backend/app/api/v1/fail2ban.go delete mode 100644 backend/app/api/v1/favorite.go delete mode 100644 backend/app/api/v1/file.go delete mode 100644 backend/app/api/v1/firewall.go delete mode 100644 backend/app/api/v1/ftp.go delete mode 100644 backend/app/api/v1/group.go delete mode 100644 backend/app/api/v1/helper/helper.go delete mode 100644 backend/app/api/v1/host.go delete mode 100644 backend/app/api/v1/host_tool.go delete mode 100644 backend/app/api/v1/image.go delete mode 100644 backend/app/api/v1/image_repo.go delete mode 100644 backend/app/api/v1/logs.go delete mode 100644 backend/app/api/v1/monitor.go delete mode 100644 backend/app/api/v1/nginx.go delete mode 100644 backend/app/api/v1/php_extensions.go delete mode 100644 backend/app/api/v1/process.go delete mode 100644 backend/app/api/v1/recycle_bin.go delete mode 100644 backend/app/api/v1/runtime.go delete mode 100644 backend/app/api/v1/setting.go delete mode 100644 backend/app/api/v1/snapshot.go delete mode 100644 backend/app/api/v1/ssh.go delete mode 100644 backend/app/api/v1/terminal.go delete mode 100644 backend/app/api/v1/website.go delete mode 100644 backend/app/api/v1/website_acme_account.go delete mode 100644 backend/app/api/v1/website_ca.go delete mode 100644 backend/app/api/v1/website_dns_account.go delete mode 100644 backend/app/api/v1/website_domain.go delete mode 100644 backend/app/api/v1/website_ssl.go delete mode 100644 backend/app/dto/app.go delete mode 100644 backend/app/dto/backup.go delete mode 100644 backend/app/dto/clam.go delete mode 100644 backend/app/dto/command.go delete mode 100644 backend/app/dto/common_req.go delete mode 100644 backend/app/dto/common_res.go delete mode 100644 backend/app/dto/compose_template.go delete mode 100644 backend/app/dto/container.go delete mode 100644 backend/app/dto/cronjob.go delete mode 100644 backend/app/dto/dashboard.go delete mode 100644 backend/app/dto/database.go delete mode 100644 backend/app/dto/database_postgresql.go delete mode 100644 backend/app/dto/device.go delete mode 100644 backend/app/dto/docker.go delete mode 100644 backend/app/dto/fail2ban.go delete mode 100644 backend/app/dto/firewall.go delete mode 100644 backend/app/dto/ftp.go delete mode 100644 backend/app/dto/group.go delete mode 100644 backend/app/dto/host.go delete mode 100644 backend/app/dto/image.go delete mode 100644 backend/app/dto/image_repo.go delete mode 100644 backend/app/dto/logs.go delete mode 100644 backend/app/dto/monitor.go delete mode 100644 backend/app/dto/nginx.go delete mode 100644 backend/app/dto/request/app.go delete mode 100644 backend/app/dto/request/favorite.go delete mode 100644 backend/app/dto/request/file.go delete mode 100644 backend/app/dto/request/host_tool.go delete mode 100644 backend/app/dto/request/nginx.go delete mode 100644 backend/app/dto/request/php_extensions.go delete mode 100644 backend/app/dto/request/process.go delete mode 100644 backend/app/dto/request/recycle_bin.go delete mode 100644 backend/app/dto/request/runtime.go delete mode 100644 backend/app/dto/request/website.go delete mode 100644 backend/app/dto/request/website_ssl.go delete mode 100644 backend/app/dto/response/app.go delete mode 100644 backend/app/dto/response/favorite.go delete mode 100644 backend/app/dto/response/file.go delete mode 100644 backend/app/dto/response/host_tool.go delete mode 100644 backend/app/dto/response/nginx.go delete mode 100644 backend/app/dto/response/php_extensions.go delete mode 100644 backend/app/dto/response/recycle_bin.go delete mode 100644 backend/app/dto/response/runtime.go delete mode 100644 backend/app/dto/response/website.go delete mode 100644 backend/app/dto/response/website_ssl.go delete mode 100644 backend/app/dto/setting.go delete mode 100644 backend/app/dto/ssh.go delete mode 100644 backend/app/model/app.go delete mode 100644 backend/app/model/app_detail.go delete mode 100644 backend/app/model/app_install.go delete mode 100644 backend/app/model/app_install_resource.go delete mode 100644 backend/app/model/app_tag.go delete mode 100644 backend/app/model/backup.go delete mode 100644 backend/app/model/base.go delete mode 100644 backend/app/model/clam.go delete mode 100644 backend/app/model/command.go delete mode 100644 backend/app/model/compose_template.go delete mode 100644 backend/app/model/cronjob.go delete mode 100644 backend/app/model/database.go delete mode 100644 backend/app/model/database_mysql.go delete mode 100644 backend/app/model/database_postgresql.go delete mode 100644 backend/app/model/favorite.go delete mode 100644 backend/app/model/firewall.go delete mode 100644 backend/app/model/ftp.go delete mode 100644 backend/app/model/group.go delete mode 100644 backend/app/model/host.go delete mode 100644 backend/app/model/image_repo.go delete mode 100644 backend/app/model/monitor.go delete mode 100644 backend/app/model/php_extensions.go delete mode 100644 backend/app/model/runtime.go delete mode 100644 backend/app/model/setting.go delete mode 100644 backend/app/model/snapshot.go delete mode 100644 backend/app/model/tag.go delete mode 100644 backend/app/model/website.go delete mode 100644 backend/app/model/website_acme_account.go delete mode 100644 backend/app/model/website_ca.go delete mode 100644 backend/app/model/website_dns_account.go delete mode 100644 backend/app/model/website_domain.go delete mode 100644 backend/app/model/website_ssl.go delete mode 100644 backend/app/repo/app.go delete mode 100644 backend/app/repo/app_detail.go delete mode 100644 backend/app/repo/app_install.go delete mode 100644 backend/app/repo/app_install_resource.go delete mode 100644 backend/app/repo/app_tag.go delete mode 100644 backend/app/repo/backup.go delete mode 100644 backend/app/repo/clam.go delete mode 100644 backend/app/repo/command.go delete mode 100644 backend/app/repo/common.go delete mode 100644 backend/app/repo/compose_template.go delete mode 100644 backend/app/repo/cronjob.go delete mode 100644 backend/app/repo/database.go delete mode 100644 backend/app/repo/database_mysql.go delete mode 100644 backend/app/repo/database_postgresql.go delete mode 100644 backend/app/repo/favorite.go delete mode 100644 backend/app/repo/ftp.go delete mode 100644 backend/app/repo/group.go delete mode 100644 backend/app/repo/host.go delete mode 100644 backend/app/repo/image_repo.go delete mode 100644 backend/app/repo/php_extensions.go delete mode 100644 backend/app/repo/runtime.go delete mode 100644 backend/app/repo/setting.go delete mode 100644 backend/app/repo/snapshot.go delete mode 100644 backend/app/repo/tag.go delete mode 100644 backend/app/repo/website.go delete mode 100644 backend/app/repo/website_acme_account.go delete mode 100644 backend/app/repo/website_ca.go delete mode 100644 backend/app/repo/website_dns_account.go delete mode 100644 backend/app/repo/website_domain.go delete mode 100644 backend/app/repo/website_ssl.go delete mode 100644 backend/app/service/app.go delete mode 100644 backend/app/service/app_install.go delete mode 100644 backend/app/service/app_utils.go delete mode 100644 backend/app/service/backup.go delete mode 100644 backend/app/service/backup_app.go delete mode 100644 backend/app/service/backup_mysql.go delete mode 100644 backend/app/service/backup_postgresql.go delete mode 100644 backend/app/service/backup_redis.go delete mode 100644 backend/app/service/backup_runtime.go delete mode 100644 backend/app/service/backup_website.go delete mode 100644 backend/app/service/clam.go delete mode 100644 backend/app/service/command.go delete mode 100644 backend/app/service/compose_template.go delete mode 100644 backend/app/service/container.go delete mode 100644 backend/app/service/container_compose.go delete mode 100644 backend/app/service/container_network.go delete mode 100644 backend/app/service/container_volume.go delete mode 100644 backend/app/service/cronjob.go delete mode 100644 backend/app/service/cronjob_backup.go delete mode 100644 backend/app/service/cronjob_helper.go delete mode 100644 backend/app/service/dashboard.go delete mode 100644 backend/app/service/database.go delete mode 100644 backend/app/service/database_common.go delete mode 100644 backend/app/service/database_mysql.go delete mode 100644 backend/app/service/database_postgresql.go delete mode 100644 backend/app/service/database_redis.go delete mode 100644 backend/app/service/device.go delete mode 100644 backend/app/service/device_clean.go delete mode 100644 backend/app/service/docker.go delete mode 100644 backend/app/service/entry.go delete mode 100644 backend/app/service/fail2ban.go delete mode 100644 backend/app/service/favorite.go delete mode 100644 backend/app/service/file.go delete mode 100644 backend/app/service/firewall.go delete mode 100644 backend/app/service/ftp.go delete mode 100644 backend/app/service/group.go delete mode 100644 backend/app/service/helper.go delete mode 100644 backend/app/service/host.go delete mode 100644 backend/app/service/host_tool.go delete mode 100644 backend/app/service/image.go delete mode 100644 backend/app/service/image_repo.go delete mode 100644 backend/app/service/logs.go delete mode 100644 backend/app/service/monitor.go delete mode 100644 backend/app/service/nginx.go delete mode 100644 backend/app/service/nginx_utils.go delete mode 100644 backend/app/service/php_extensions.go delete mode 100644 backend/app/service/process.go delete mode 100644 backend/app/service/recycle_bin.go delete mode 100644 backend/app/service/runtime.go delete mode 100644 backend/app/service/runtime_utils.go delete mode 100644 backend/app/service/setting.go delete mode 100644 backend/app/service/snapshot.go delete mode 100644 backend/app/service/snapshot_create.go delete mode 100644 backend/app/service/snapshot_recover.go delete mode 100644 backend/app/service/ssh.go delete mode 100644 backend/app/service/website.go delete mode 100644 backend/app/service/website_acme_account.go delete mode 100644 backend/app/service/website_ca.go delete mode 100644 backend/app/service/website_dns_account.go delete mode 100644 backend/app/service/website_ssl.go delete mode 100644 backend/app/service/website_utils.go delete mode 100644 backend/app/task/task.go delete mode 100644 backend/buserr/errors.go delete mode 100644 backend/buserr/multi_err.go delete mode 100644 backend/configs/config.go delete mode 100644 backend/configs/log.go delete mode 100644 backend/configs/system.go delete mode 100644 backend/constant/app.go delete mode 100644 backend/constant/backup.go delete mode 100644 backend/constant/common.go delete mode 100644 backend/constant/container.go delete mode 100644 backend/constant/dir.go delete mode 100644 backend/constant/errs.go delete mode 100644 backend/constant/host_tool.go delete mode 100644 backend/constant/nginx.go delete mode 100644 backend/constant/runtime.go delete mode 100644 backend/constant/session.go delete mode 100644 backend/constant/status.go delete mode 100644 backend/constant/website.go delete mode 100644 backend/cron/cron.go delete mode 100644 backend/cron/job/app.go delete mode 100644 backend/cron/job/cache.go delete mode 100644 backend/cron/job/ssl.go delete mode 100644 backend/cron/job/website.go delete mode 100644 backend/global/global.go delete mode 100644 backend/i18n/i18n.go delete mode 100644 backend/i18n/lang/en.yaml delete mode 100644 backend/i18n/lang/zh-Hant.yaml delete mode 100644 backend/i18n/lang/zh.yaml delete mode 100644 backend/init/app/app.go delete mode 100644 backend/init/business/business.go delete mode 100644 backend/init/cache/badger_db/badger_db.go delete mode 100644 backend/init/cache/cache.go delete mode 100644 backend/init/db/db.go delete mode 100644 backend/init/hook/hook.go delete mode 100644 backend/init/log/log.go delete mode 100644 backend/init/migration/migrate.go delete mode 100644 backend/init/migration/migrations/init.go delete mode 100644 backend/init/router/router.go delete mode 100644 backend/init/validator/validator.go delete mode 100644 backend/init/viper/viper.go delete mode 100644 backend/log/config.go delete mode 100644 backend/log/dup_write_darwin.go delete mode 100644 backend/log/dup_write_linux.go delete mode 100644 backend/log/dup_write_windows.go delete mode 100644 backend/log/manager.go delete mode 100644 backend/log/writer.go delete mode 100644 backend/middleware/demo_handle.go delete mode 100644 backend/middleware/helper.go delete mode 100644 backend/middleware/loading.go delete mode 100644 backend/router/common.go delete mode 100644 backend/router/entry.go delete mode 100644 backend/router/ro_app.go delete mode 100644 backend/router/ro_container.go delete mode 100644 backend/router/ro_cronjob.go delete mode 100644 backend/router/ro_dashboard.go delete mode 100644 backend/router/ro_database.go delete mode 100644 backend/router/ro_file.go delete mode 100644 backend/router/ro_group.go delete mode 100644 backend/router/ro_host.go delete mode 100644 backend/router/ro_log.go delete mode 100644 backend/router/ro_nginx.go delete mode 100644 backend/router/ro_process.go delete mode 100644 backend/router/ro_router.go delete mode 100644 backend/router/ro_runtime.go delete mode 100644 backend/router/ro_setting.go delete mode 100644 backend/router/ro_terminal.go delete mode 100644 backend/router/ro_toolbox.go delete mode 100644 backend/router/ro_website.go delete mode 100644 backend/router/ro_website_acme_account.go delete mode 100644 backend/router/ro_website_ca.go delete mode 100644 backend/router/ro_website_dns_account.go delete mode 100644 backend/router/ro_website_ssl.go delete mode 100644 backend/server/init.go delete mode 100644 backend/server/server.go delete mode 100644 backend/utils/cloud_storage/client/cos.go delete mode 100644 backend/utils/cloud_storage/client/helper.go delete mode 100644 backend/utils/cloud_storage/client/kodo.go delete mode 100644 backend/utils/cloud_storage/client/local.go delete mode 100644 backend/utils/cloud_storage/client/minio.go delete mode 100644 backend/utils/cloud_storage/client/onedrive.go delete mode 100644 backend/utils/cloud_storage/client/oss.go delete mode 100644 backend/utils/cloud_storage/client/s3.go delete mode 100644 backend/utils/cloud_storage/client/sftp.go delete mode 100644 backend/utils/cloud_storage/client/webdav.go delete mode 100644 backend/utils/cloud_storage/cloud_storage_client.go delete mode 100644 backend/utils/cmd/cmd.go delete mode 100644 backend/utils/common/common.go delete mode 100644 backend/utils/compose/compose.go delete mode 100644 backend/utils/copier/copier.go delete mode 100644 backend/utils/docker/compose.go delete mode 100644 backend/utils/docker/docker.go delete mode 100644 backend/utils/encrypt/encrypt.go delete mode 100644 backend/utils/env/env.go delete mode 100644 backend/utils/files/archiver.go delete mode 100644 backend/utils/files/file_op.go delete mode 100644 backend/utils/files/fileinfo.go delete mode 100644 backend/utils/files/tar.go delete mode 100644 backend/utils/files/tar_gz.go delete mode 100644 backend/utils/files/utils.go delete mode 100644 backend/utils/files/zip.go delete mode 100644 backend/utils/firewall/client.go delete mode 100644 backend/utils/firewall/client/firewalld.go delete mode 100644 backend/utils/firewall/client/info.go delete mode 100644 backend/utils/firewall/client/iptables.go delete mode 100644 backend/utils/firewall/client/ufw.go delete mode 100644 backend/utils/http/get.go delete mode 100644 backend/utils/http/request.go delete mode 100644 backend/utils/ini_conf/ini.go delete mode 100644 backend/utils/jwt/jwt.go delete mode 100644 backend/utils/mysql/client.go delete mode 100644 backend/utils/mysql/client/info.go delete mode 100644 backend/utils/mysql/client/local.go delete mode 100644 backend/utils/mysql/client/remote.go delete mode 100644 backend/utils/mysql/helper/dump.go delete mode 100644 backend/utils/mysql/helper/source.go delete mode 100644 backend/utils/nginx/components/block.go delete mode 100644 backend/utils/nginx/components/comment.go delete mode 100644 backend/utils/nginx/components/config.go delete mode 100644 backend/utils/nginx/components/directive.go delete mode 100644 backend/utils/nginx/components/http.go delete mode 100644 backend/utils/nginx/components/location.go delete mode 100644 backend/utils/nginx/components/lua_block.go delete mode 100644 backend/utils/nginx/components/server.go delete mode 100644 backend/utils/nginx/components/server_listen.go delete mode 100644 backend/utils/nginx/components/statement.go delete mode 100644 backend/utils/nginx/components/upstream.go delete mode 100644 backend/utils/nginx/components/upstream_server.go delete mode 100644 backend/utils/nginx/dumper.go delete mode 100644 backend/utils/nginx/parser/flag/flag.go delete mode 100644 backend/utils/nginx/parser/lexer.go delete mode 100644 backend/utils/nginx/parser/parser.go delete mode 100644 backend/utils/ntp/ntp.go delete mode 100644 backend/utils/postgresql/client.go delete mode 100644 backend/utils/postgresql/client/info.go delete mode 100644 backend/utils/postgresql/client/local.go delete mode 100644 backend/utils/postgresql/client/remote.go delete mode 100644 backend/utils/qqwry/qqwry.go delete mode 100644 backend/utils/redis/redis.go delete mode 100644 backend/utils/ssh/ssh.go delete mode 100644 backend/utils/ssl/acme.go delete mode 100644 backend/utils/ssl/client.go delete mode 100644 backend/utils/systemctl/systemctl.go delete mode 100644 backend/utils/terminal/local_cmd.go delete mode 100644 backend/utils/terminal/ws_local_session.go delete mode 100644 backend/utils/terminal/ws_session.go delete mode 100644 backend/utils/toolbox/fail2ban.go delete mode 100644 backend/utils/toolbox/pure-ftpd.go delete mode 100644 backend/utils/websocket/client.go delete mode 100644 backend/utils/websocket/process_data.go delete mode 100644 backend/utils/xpack/xpack.go delete mode 100644 cmd/server/app/app_config.go delete mode 100644 cmd/server/app/app_config.yml delete mode 100644 cmd/server/app/app_param.yml delete mode 100644 cmd/server/app/logo.png rename {core/cmd => cmd}/server/cmd/listen-ip.go (100%) rename {core/cmd => cmd}/server/cmd/reset.go (100%) rename {core/cmd => cmd}/server/cmd/restore.go (100%) rename {core/cmd => cmd}/server/cmd/root.go (100%) rename {core/cmd => cmd}/server/cmd/update.go (100%) rename {core/cmd => cmd}/server/cmd/user-info.go (100%) rename {core/cmd => cmd}/server/cmd/version.go (94%) rename cmd/server/{ => conf}/qqwry/qqwey.go (100%) rename cmd/server/{ => conf}/qqwry/qqwry.dat (100%) delete mode 100644 cmd/server/nginx_conf/404.html delete mode 100644 cmd/server/nginx_conf/cache.conf delete mode 100644 cmd/server/nginx_conf/domain404.html delete mode 100644 cmd/server/nginx_conf/gzip.conf delete mode 100644 cmd/server/nginx_conf/index.html delete mode 100644 cmd/server/nginx_conf/index.php delete mode 100644 cmd/server/nginx_conf/nginx_conf.go delete mode 100644 cmd/server/nginx_conf/proxy.conf delete mode 100644 cmd/server/nginx_conf/proxy_cache.conf delete mode 100644 cmd/server/nginx_conf/proxy_no_cache.conf delete mode 100644 cmd/server/nginx_conf/rewrite/crmeb.conf delete mode 100644 cmd/server/nginx_conf/rewrite/dabr.conf delete mode 100644 cmd/server/nginx_conf/rewrite/dbshop.conf delete mode 100644 cmd/server/nginx_conf/rewrite/dedecms.conf delete mode 100644 cmd/server/nginx_conf/rewrite/default.conf delete mode 100644 cmd/server/nginx_conf/rewrite/discuz.conf delete mode 100644 cmd/server/nginx_conf/rewrite/discuzx.conf delete mode 100644 cmd/server/nginx_conf/rewrite/discuzx2.conf delete mode 100644 cmd/server/nginx_conf/rewrite/discuzx3.conf delete mode 100644 cmd/server/nginx_conf/rewrite/drupal.conf delete mode 100644 cmd/server/nginx_conf/rewrite/ecshop.conf delete mode 100644 cmd/server/nginx_conf/rewrite/edusoho.conf delete mode 100644 cmd/server/nginx_conf/rewrite/emlog.conf delete mode 100644 cmd/server/nginx_conf/rewrite/empirecms.conf delete mode 100644 cmd/server/nginx_conf/rewrite/laravel5.conf delete mode 100644 cmd/server/nginx_conf/rewrite/maccms.conf delete mode 100644 cmd/server/nginx_conf/rewrite/mvc.conf delete mode 100644 cmd/server/nginx_conf/rewrite/niushop.conf delete mode 100644 cmd/server/nginx_conf/rewrite/phpcms.conf delete mode 100644 cmd/server/nginx_conf/rewrite/phpwind.conf delete mode 100644 cmd/server/nginx_conf/rewrite/sablog.conf delete mode 100644 cmd/server/nginx_conf/rewrite/seacms.conf delete mode 100644 cmd/server/nginx_conf/rewrite/shopex.conf delete mode 100644 cmd/server/nginx_conf/rewrite/shopwind.conf delete mode 100644 cmd/server/nginx_conf/rewrite/thinkphp.conf delete mode 100644 cmd/server/nginx_conf/rewrite/typecho.conf delete mode 100644 cmd/server/nginx_conf/rewrite/typecho2.conf delete mode 100644 cmd/server/nginx_conf/rewrite/wordpress.conf delete mode 100644 cmd/server/nginx_conf/rewrite/wp2.conf delete mode 100644 cmd/server/nginx_conf/rewrite/yii2.conf delete mode 100644 cmd/server/nginx_conf/rewrite/zblog.conf delete mode 100644 cmd/server/nginx_conf/ssl.conf delete mode 100644 cmd/server/nginx_conf/stop.html delete mode 100644 cmd/server/nginx_conf/website_default.conf rename {core/cmd => cmd}/server/web/favicon.png (100%) rename {core/cmd => cmd}/server/web/web.go (100%) delete mode 100644 core/cmd/server/conf/app.yaml delete mode 100644 core/cmd/server/conf/conf.go delete mode 100644 core/cmd/server/conf/qqwry/qqwey.go delete mode 100644 core/cmd/server/conf/qqwry/qqwry.dat delete mode 100644 core/cmd/server/docs/docs.go delete mode 100644 core/cmd/server/docs/swagger.go delete mode 100644 core/cmd/server/docs/swagger.json delete mode 100644 core/cmd/server/docs/swagger.yaml delete mode 100644 core/cmd/server/main.go delete mode 100644 core/go.mod delete mode 100644 core/go.sum diff --git a/backend/app/api/v1/app.go b/backend/app/api/v1/app.go deleted file mode 100644 index 345dbd1ff..000000000 --- a/backend/app/api/v1/app.go +++ /dev/null @@ -1,197 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/i18n" - "github.com/gin-gonic/gin" -) - -// @Tags App -// @Summary List apps -// @Description 获取应用列表 -// @Accept json -// @Param request body request.AppSearch true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /apps/search [post] -func (b *BaseApi) SearchApp(c *gin.Context) { - var req request.AppSearch - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - list, err := appService.PageApp(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, list) -} - -// @Tags App -// @Summary Sync app list -// @Description 同步应用列表 -// @Success 200 -// @Security ApiKeyAuth -// @Router /apps/sync [post] -// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"应用商店同步","formatEN":"App store synchronization"} -func (b *BaseApi) SyncApp(c *gin.Context) { - go appService.SyncAppListFromLocal() - res, err := appService.GetAppUpdate() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - if !res.CanUpdate { - if res.IsSyncing { - helper.SuccessWithMsg(c, i18n.GetMsgByKey("AppStoreIsSyncing")) - } else { - helper.SuccessWithMsg(c, i18n.GetMsgByKey("AppStoreIsUpToDate")) - } - return - } - go func() { - if err := appService.SyncAppListFromRemote(); err != nil { - global.LOG.Errorf("Synchronization with the App Store failed [%s]", err.Error()) - } - }() - helper.SuccessWithData(c, "") -} - -// @Tags App -// @Summary Search app by key -// @Description 通过 key 获取应用信息 -// @Accept json -// @Param key path string true "app key" -// @Success 200 {object} response.AppDTO -// @Security ApiKeyAuth -// @Router /apps/:key [get] -func (b *BaseApi) GetApp(c *gin.Context) { - appKey, err := helper.GetStrParamByKey(c, "key") - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - appDTO, err := appService.GetApp(appKey) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, appDTO) -} - -// @Tags App -// @Summary Search app detail by appid -// @Description 通过 appid 获取应用详情 -// @Accept json -// @Param appId path integer true "app id" -// @Param version path string true "app 版本" -// @Param version path string true "app 类型" -// @Success 200 {object} response.AppDetailDTO -// @Security ApiKeyAuth -// @Router /apps/detail/:appId/:version/:type [get] -func (b *BaseApi) GetAppDetail(c *gin.Context) { - appID, err := helper.GetIntParamByKey(c, "appId") - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) - return - } - version := c.Param("version") - appType := c.Param("type") - appDetailDTO, err := appService.GetAppDetail(appID, version, appType) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, appDetailDTO) -} - -// @Tags App -// @Summary Get app detail by id -// @Description 通过 id 获取应用详情 -// @Accept json -// @Param appId path integer true "id" -// @Success 200 {object} response.AppDetailDTO -// @Security ApiKeyAuth -// @Router /apps/details/:id [get] -func (b *BaseApi) GetAppDetailByID(c *gin.Context) { - appDetailID, err := helper.GetIntParamByKey(c, "id") - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) - return - } - appDetailDTO, err := appService.GetAppDetailByID(appDetailID) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, appDetailDTO) -} - -// @Tags App -// @Summary Get Ignore App -// @Description 获取忽略的应用版本 -// @Accept json -// @Success 200 {object} response.IgnoredApp -// @Security ApiKeyAuth -// @Router /apps/ignored [get] -func (b *BaseApi) GetIgnoredApp(c *gin.Context) { - res, err := appService.GetIgnoredApp() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, res) -} - -// @Tags App -// @Summary Install app -// @Description 安装应用 -// @Accept json -// @Param request body request.AppInstallCreate true "request" -// @Success 200 {object} model.AppInstall -// @Security ApiKeyAuth -// @Router /apps/install [post] -// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[{"input_column":"name","input_value":"name","isList":false,"db":"app_installs","output_column":"app_id","output_value":"appId"},{"info":"appId","isList":false,"db":"apps","output_column":"key","output_value":"appKey"}],"formatZH":"安装应用 [appKey]-[name]","formatEN":"Install app [appKey]-[name]"} -func (b *BaseApi) InstallApp(c *gin.Context) { - var req request.AppInstallCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - tx, ctx := helper.GetTxAndContext() - install, err := appService.Install(ctx, req) - if err != nil { - tx.Rollback() - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - tx.Commit() - helper.SuccessWithData(c, install) -} - -func (b *BaseApi) GetAppTags(c *gin.Context) { - tags, err := appService.GetAppTags() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, tags) -} - -// @Tags App -// @Summary Get app list update -// @Description 获取应用更新版本 -// @Success 200 -// @Security ApiKeyAuth -// @Router /apps/checkupdate [get] -func (b *BaseApi) GetAppListUpdate(c *gin.Context) { - res, err := appService.GetAppUpdate() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, res) -} diff --git a/backend/app/api/v1/app_install.go b/backend/app/api/v1/app_install.go deleted file mode 100644 index f0b63ab6c..000000000 --- a/backend/app/api/v1/app_install.go +++ /dev/null @@ -1,327 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags App -// @Summary Page app installed -// @Description 分页获取已安装应用列表 -// @Accept json -// @Param request body request.AppInstalledSearch true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /apps/installed/search [post] -func (b *BaseApi) SearchAppInstalled(c *gin.Context) { - var req request.AppInstalledSearch - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if req.All { - list, err := appInstallService.SearchForWebsite(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, list) - } else { - total, list, err := appInstallService.Page(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) - } -} - -// @Tags App -// @Summary List app installed -// @Description 获取已安装应用列表 -// @Accept json -// @Success 200 array dto.AppInstallInfo -// @Security ApiKeyAuth -// @Router /apps/installed/list [get] -func (b *BaseApi) ListAppInstalled(c *gin.Context) { - list, err := appInstallService.GetInstallList() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, list) -} - -// @Tags App -// @Summary Check app installed -// @Description 检查应用安装情况 -// @Accept json -// @Param request body request.AppInstalledInfo true "request" -// @Success 200 {object} response.AppInstalledCheck -// @Security ApiKeyAuth -// @Router /apps/installed/check [post] -func (b *BaseApi) CheckAppInstalled(c *gin.Context) { - var req request.AppInstalledInfo - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - checkData, err := appInstallService.CheckExist(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, checkData) -} - -// @Tags App -// @Summary Search app port by key -// @Description 获取应用端口 -// @Accept json -// @Param request body dto.OperationWithNameAndType true "request" -// @Success 200 {integer} port -// @Security ApiKeyAuth -// @Router /apps/installed/loadport [post] -func (b *BaseApi) LoadPort(c *gin.Context) { - var req dto.OperationWithNameAndType - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - port, err := appInstallService.LoadPort(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, port) -} - -// @Tags App -// @Summary Search app password by key -// @Description 获取应用连接信息 -// @Accept json -// @Param request body dto.OperationWithNameAndType true "request" -// @Success 200 {string} response.DatabaseConn -// @Security ApiKeyAuth -// @Router /apps/installed/conninfo/:key [get] -func (b *BaseApi) LoadConnInfo(c *gin.Context) { - var req dto.OperationWithNameAndType - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - conn, err := appInstallService.LoadConnInfo(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, conn) -} - -// @Tags App -// @Summary Check before delete -// @Description 删除前检查 -// @Accept json -// @Param appInstallId path integer true "App install id" -// @Success 200 {array} dto.AppResource -// @Security ApiKeyAuth -// @Router /apps/installed/delete/check/:appInstallId [get] -func (b *BaseApi) DeleteCheck(c *gin.Context) { - appInstallId, err := helper.GetIntParamByKey(c, "appInstallId") - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) - return - } - checkData, err := appInstallService.DeleteCheck(appInstallId) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, checkData) -} - -// Sync app installed -// @Tags App -// @Summary Sync app installed -// @Description 同步已安装应用列表 -// @Success 200 -// @Security ApiKeyAuth -// @Router /apps/installed/sync [post] -// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步已安装应用列表","formatEN":"Sync the list of installed apps"} -func (b *BaseApi) SyncInstalled(c *gin.Context) { - if err := appInstallService.SyncAll(false); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, "") -} - -// @Tags App -// @Summary Operate installed app -// @Description 操作已安装应用 -// @Accept json -// @Param request body request.AppInstalledOperate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /apps/installed/op [post] -// @x-panel-log {"bodyKeys":["installId","operate"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"installId","isList":false,"db":"app_installs","output_column":"app_id","output_value":"appId"},{"input_column":"id","input_value":"installId","isList":false,"db":"app_installs","output_column":"name","output_value":"appName"},{"input_column":"id","input_value":"appId","isList":false,"db":"apps","output_column":"key","output_value":"appKey"}],"formatZH":"[operate] 应用 [appKey][appName]","formatEN":"[operate] App [appKey][appName]"} -func (b *BaseApi) OperateInstalled(c *gin.Context) { - var req request.AppInstalledOperate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := appInstallService.Operate(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags App -// @Summary Search app service by key -// @Description 通过 key 获取应用 service -// @Accept json -// @Param key path string true "request" -// @Success 200 {array} response.AppService -// @Security ApiKeyAuth -// @Router /apps/services/:key [get] -func (b *BaseApi) GetServices(c *gin.Context) { - key := c.Param("key") - services, err := appInstallService.GetServices(key) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, services) -} - -// @Tags App -// @Summary Search app update version by install id -// @Description 通过 install id 获取应用更新版本 -// @Accept json -// @Param appInstallId path integer true "request" -// @Success 200 {array} dto.AppVersion -// @Security ApiKeyAuth -// @Router /apps/installed/update/versions [post] -func (b *BaseApi) GetUpdateVersions(c *gin.Context) { - var req request.AppUpdateVersion - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - versions, err := appInstallService.GetUpdateVersions(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, versions) -} - -// @Tags App -// @Summary Change app port -// @Description 修改应用端口 -// @Accept json -// @Param request body request.PortUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /apps/installed/port/change [post] -// @x-panel-log {"bodyKeys":["key","name","port"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"应用端口修改 [key]-[name] => [port]","formatEN":"Application port update [key]-[name] => [port]"} -func (b *BaseApi) ChangeAppPort(c *gin.Context) { - var req request.PortUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := appInstallService.ChangeAppPort(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags App -// @Summary Search default config by key -// @Description 通过 key 获取应用默认配置 -// @Accept json -// @Param request body dto.OperationWithNameAndType true "request" -// @Success 200 {string} content -// @Security ApiKeyAuth -// @Router /apps/installed/conf [post] -func (b *BaseApi) GetDefaultConfig(c *gin.Context) { - var req dto.OperationWithNameAndType - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - content, err := appInstallService.GetDefaultConfigByKey(req.Type, req.Name) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, content) -} - -// @Tags App -// @Summary Search params by appInstallId -// @Description 通过 install id 获取应用参数 -// @Accept json -// @Param appInstallId path string true "request" -// @Success 200 {object} response.AppParam -// @Security ApiKeyAuth -// @Router /apps/installed/params/:appInstallId [get] -func (b *BaseApi) GetParams(c *gin.Context) { - appInstallId, err := helper.GetIntParamByKey(c, "appInstallId") - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) - return - } - content, err := appInstallService.GetParams(appInstallId) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, content) -} - -// @Tags App -// @Summary Change app params -// @Description 修改应用参数 -// @Accept json -// @Param request body request.AppInstalledUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /apps/installed/params/update [post] -// @x-panel-log {"bodyKeys":["installId"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"应用参数修改 [installId]","formatEN":"Application param update [installId]"} -func (b *BaseApi) UpdateInstalled(c *gin.Context) { - var req request.AppInstalledUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := appInstallService.Update(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags App -// @Summary ignore App Update -// @Description 忽略应用升级版本 -// @Accept json -// @Param request body request.AppInstalledIgnoreUpgrade true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /apps/installed/ignore [post] -// @x-panel-log {"bodyKeys":["installId"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"忽略应用 [installId] 版本升级","formatEN":"Application param update [installId]"} -func (b *BaseApi) IgnoreUpgrade(c *gin.Context) { - var req request.AppInstalledIgnoreUpgrade - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := appInstallService.IgnoreUpgrade(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} diff --git a/backend/app/api/v1/backup.go b/backend/app/api/v1/backup.go deleted file mode 100644 index 1849ba568..000000000 --- a/backend/app/api/v1/backup.go +++ /dev/null @@ -1,443 +0,0 @@ -package v1 - -import ( - "encoding/base64" - "fmt" - "path" - - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Backup Account -// @Summary Create backup account -// @Description 创建备份账号 -// @Accept json -// @Param request body dto.BackupOperate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /settings/backup [post] -// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建备份账号 [type]","formatEN":"create backup account [type]"} -func (b *BaseApi) CreateBackup(c *gin.Context) { - var req dto.BackupOperate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if len(req.Credential) != 0 { - credential, err := base64.StdEncoding.DecodeString(req.Credential) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - req.Credential = string(credential) - } - if len(req.AccessKey) != 0 { - accessKey, err := base64.StdEncoding.DecodeString(req.AccessKey) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - req.AccessKey = string(accessKey) - } - - if err := backupService.Create(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Backup Account -// @Summary Refresh OneDrive token -// @Description 刷新 OneDrive token -// @Success 200 -// @Security ApiKeyAuth -// @Router /settings/backup/refresh/onedrive [post] -func (b *BaseApi) RefreshOneDriveToken(c *gin.Context) { - backupService.Run() - helper.SuccessWithData(c, nil) -} - -// @Tags Backup Account -// @Summary List buckets -// @Description 获取 bucket 列表 -// @Accept json -// @Param request body dto.ForBuckets true "request" -// @Success 200 {array} string -// @Security ApiKeyAuth -// @Router /settings/backup/search [post] -func (b *BaseApi) ListBuckets(c *gin.Context) { - var req dto.ForBuckets - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if len(req.Credential) != 0 { - credential, err := base64.StdEncoding.DecodeString(req.Credential) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - req.Credential = string(credential) - } - if len(req.AccessKey) != 0 { - accessKey, err := base64.StdEncoding.DecodeString(req.AccessKey) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - req.AccessKey = string(accessKey) - } - - buckets, err := backupService.GetBuckets(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, buckets) -} - -// @Tags Backup Account -// @Summary Load OneDrive info -// @Description 获取 OneDrive 信息 -// @Accept json -// @Success 200 {object} dto.OneDriveInfo -// @Security ApiKeyAuth -// @Router /settings/backup/onedrive [get] -func (b *BaseApi) LoadOneDriveInfo(c *gin.Context) { - data, err := backupService.LoadOneDriveInfo() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, data) -} - -// @Tags Backup Account -// @Summary Delete backup account -// @Description 删除备份账号 -// @Accept json -// @Param request body dto.OperateByID true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /settings/backup/del [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"backup_accounts","output_column":"type","output_value":"types"}],"formatZH":"删除备份账号 [types]","formatEN":"delete backup account [types]"} -func (b *BaseApi) DeleteBackup(c *gin.Context) { - var req dto.OperateByID - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := backupService.Delete(req.ID); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Backup Account -// @Summary Page backup records -// @Description 获取备份记录列表分页 -// @Accept json -// @Param request body dto.RecordSearch true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /settings/backup/record/search [post] -func (b *BaseApi) SearchBackupRecords(c *gin.Context) { - var req dto.RecordSearch - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - total, list, err := backupService.SearchRecordsWithPage(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - -// @Tags Backup Account -// @Summary Page backup records by cronjob -// @Description 通过计划任务获取备份记录列表分页 -// @Accept json -// @Param request body dto.RecordSearchByCronjob true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /settings/backup/record/search/bycronjob [post] -func (b *BaseApi) SearchBackupRecordsByCronjob(c *gin.Context) { - var req dto.RecordSearchByCronjob - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - total, list, err := backupService.SearchRecordsByCronjobWithPage(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - -// @Tags Backup Account -// @Summary Download backup record -// @Description 下载备份记录 -// @Accept json -// @Param request body dto.DownloadRecord true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /settings/backup/record/download [post] -// @x-panel-log {"bodyKeys":["source","fileName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"下载备份记录 [source][fileName]","formatEN":"download backup records [source][fileName]"} -func (b *BaseApi) DownloadRecord(c *gin.Context) { - var req dto.DownloadRecord - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - filePath, err := backupService.DownloadRecord(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, filePath) -} - -// @Tags Backup Account -// @Summary Delete backup record -// @Description 删除备份记录 -// @Accept json -// @Param request body dto.BatchDeleteReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /settings/backup/record/del [post] -// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"backup_records","output_column":"file_name","output_value":"files"}],"formatZH":"删除备份记录 [files]","formatEN":"delete backup records [files]"} -func (b *BaseApi) DeleteBackupRecord(c *gin.Context) { - var req dto.BatchDeleteReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := backupService.BatchDeleteRecord(req.Ids); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Backup Account -// @Summary Update backup account -// @Description 更新备份账号信息 -// @Accept json -// @Param request body dto.BackupOperate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /settings/backup/update [post] -// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新备份账号 [types]","formatEN":"update backup account [types]"} -func (b *BaseApi) UpdateBackup(c *gin.Context) { - var req dto.BackupOperate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if len(req.Credential) != 0 { - credential, err := base64.StdEncoding.DecodeString(req.Credential) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - req.Credential = string(credential) - } - if len(req.AccessKey) != 0 { - accessKey, err := base64.StdEncoding.DecodeString(req.AccessKey) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - req.AccessKey = string(accessKey) - } - - if err := backupService.Update(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Backup Account -// @Summary List backup accounts -// @Description 获取备份账号列表 -// @Success 200 {array} dto.BackupInfo -// @Security ApiKeyAuth -// @Router /settings/backup/search [get] -func (b *BaseApi) ListBackup(c *gin.Context) { - data, err := backupService.List() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, data) -} - -// @Tags Backup Account -// @Summary List files from backup accounts -// @Description 获取备份账号内文件列表 -// @Accept json -// @Param request body dto.BackupSearchFile true "request" -// @Success 200 {array} string -// @Security ApiKeyAuth -// @Router /settings/backup/search/files [post] -func (b *BaseApi) LoadFilesFromBackup(c *gin.Context) { - var req dto.BackupSearchFile - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - data := backupService.ListFiles(req) - helper.SuccessWithData(c, data) -} - -// @Tags Backup Account -// @Summary Backup system data -// @Description 备份系统数据 -// @Accept json -// @Param request body dto.CommonBackup true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /settings/backup/backup [post] -// @x-panel-log {"bodyKeys":["type","name","detailName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"备份 [type] 数据 [name][detailName]","formatEN":"backup [type] data [name][detailName]"} -func (b *BaseApi) Backup(c *gin.Context) { - var req dto.CommonBackup - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - switch req.Type { - case "app": - if _, err := backupService.AppBackup(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - case "mysql", "mariadb": - if err := backupService.MysqlBackup(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - case constant.AppPostgresql: - if err := backupService.PostgresqlBackup(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - case "website": - if err := backupService.WebsiteBackup(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - case "redis": - if err := backupService.RedisBackup(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - } - helper.SuccessWithData(c, nil) -} - -// @Tags Backup Account -// @Summary Recover system data -// @Description 恢复系统数据 -// @Accept json -// @Param request body dto.CommonRecover true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /settings/backup/recover [post] -// @x-panel-log {"bodyKeys":["type","name","detailName","file"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"从 [file] 恢复 [type] 数据 [name][detailName]","formatEN":"recover [type] data [name][detailName] from [file]"} -func (b *BaseApi) Recover(c *gin.Context) { - var req dto.CommonRecover - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - downloadPath, err := backupService.DownloadRecord(dto.DownloadRecord{Source: req.Source, FileDir: path.Dir(req.File), FileName: path.Base(req.File)}) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, fmt.Errorf("download file failed, err: %v", err)) - return - } - req.File = downloadPath - switch req.Type { - case "mysql", "mariadb": - if err := backupService.MysqlRecover(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - case constant.AppPostgresql: - if err := backupService.PostgresqlRecover(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - case "website": - if err := backupService.WebsiteRecover(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - case "redis": - if err := backupService.RedisRecover(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - case "app": - if err := backupService.AppRecover(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - } - helper.SuccessWithData(c, nil) -} - -// @Tags Backup Account -// @Summary Recover system data by upload -// @Description 从上传恢复系统数据 -// @Accept json -// @Param request body dto.CommonRecover true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /settings/backup/recover/byupload [post] -// @x-panel-log {"bodyKeys":["type","name","detailName","file"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"从 [file] 恢复 [type] 数据 [name][detailName]","formatEN":"recover [type] data [name][detailName] from [file]"} -func (b *BaseApi) RecoverByUpload(c *gin.Context) { - var req dto.CommonRecover - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - switch req.Type { - case "mysql", "mariadb": - if err := backupService.MysqlRecoverByUpload(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - case constant.AppPostgresql: - if err := backupService.PostgresqlRecoverByUpload(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - case "app": - if err := backupService.AppRecover(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - case "website": - if err := backupService.WebsiteRecover(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - } - helper.SuccessWithData(c, nil) -} diff --git a/backend/app/api/v1/clam.go b/backend/app/api/v1/clam.go deleted file mode 100644 index 0e4e7cf47..000000000 --- a/backend/app/api/v1/clam.go +++ /dev/null @@ -1,296 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Clam -// @Summary Create clam -// @Description 创建扫描规则 -// @Accept json -// @Param request body dto.ClamCreate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /toolbox/clam [post] -// @x-panel-log {"bodyKeys":["name","path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建扫描规则 [name][path]","formatEN":"create clam [name][path]"} -func (b *BaseApi) CreateClam(c *gin.Context) { - var req dto.ClamCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := clamService.Create(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Clam -// @Summary Update clam -// @Description 修改扫描规则 -// @Accept json -// @Param request body dto.ClamUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /toolbox/clam/update [post] -// @x-panel-log {"bodyKeys":["name","path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改扫描规则 [name][path]","formatEN":"update clam [name][path]"} -func (b *BaseApi) UpdateClam(c *gin.Context) { - var req dto.ClamUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := clamService.Update(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Clam -// @Summary Update clam status -// @Description 修改扫描规则状态 -// @Accept json -// @Param request body dto.ClamUpdateStatus true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /toolbox/clam/status/update [post] -// @x-panel-log {"bodyKeys":["id","status"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"clams","output_column":"name","output_value":"name"}],"formatZH":"修改扫描规则 [name] 状态为 [status]","formatEN":"change the status of clam [name] to [status]."} -func (b *BaseApi) UpdateClamStatus(c *gin.Context) { - var req dto.ClamUpdateStatus - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := clamService.UpdateStatus(req.ID, req.Status); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Clam -// @Summary Page clam -// @Description 获取扫描规则列表分页 -// @Accept json -// @Param request body dto.SearchClamWithPage true "request" -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /toolbox/clam/search [post] -func (b *BaseApi) SearchClam(c *gin.Context) { - var req dto.SearchClamWithPage - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - total, list, err := clamService.SearchWithPage(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - -// @Tags Clam -// @Summary Load clam base info -// @Description 获取 Clam 基础信息 -// @Accept json -// @Success 200 {object} dto.ClamBaseInfo -// @Security ApiKeyAuth -// @Router /toolbox/clam/base [get] -func (b *BaseApi) LoadClamBaseInfo(c *gin.Context) { - info, err := clamService.LoadBaseInfo() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, info) -} - -// @Tags Clam -// @Summary Operate Clam -// @Description 修改 Clam 状态 -// @Accept json -// @Param request body dto.Operate true "request" -// @Security ApiKeyAuth -// @Router /toolbox/clam/operate [post] -// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] Clam","formatEN":"[operation] FTP"} -func (b *BaseApi) OperateClam(c *gin.Context) { - var req dto.Operate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := clamService.Operate(req.Operation); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -// @Tags Clam -// @Summary Clean clam record -// @Description 清空扫描报告 -// @Accept json -// @Param request body dto.OperateByID true "request" -// @Security ApiKeyAuth -// @Router /toolbox/clam/record/clean [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":true,"db":"clams","output_column":"name","output_value":"name"}],"formatZH":"清空扫描报告 [name]","formatEN":"clean clam record [name]"} -func (b *BaseApi) CleanClamRecord(c *gin.Context) { - var req dto.OperateByID - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := clamService.CleanRecord(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -// @Tags Clam -// @Summary Page clam record -// @Description 获取扫描结果列表分页 -// @Accept json -// @Param request body dto.ClamLogSearch true "request" -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /toolbox/clam/record/search [post] -func (b *BaseApi) SearchClamRecord(c *gin.Context) { - var req dto.ClamLogSearch - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - total, list, err := clamService.LoadRecords(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - -// @Tags Clam -// @Summary Load clam record detail -// @Description 获取扫描结果详情 -// @Accept json -// @Param request body dto.ClamLogReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /toolbox/clam/record/log [post] -func (b *BaseApi) LoadClamRecordLog(c *gin.Context) { - var req dto.ClamLogReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - content, err := clamService.LoadRecordLog(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, content) -} - -// @Tags Clam -// @Summary Load clam file -// @Description 获取扫描文件 -// @Accept json -// @Param request body dto.ClamFileReq true "request" -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /toolbox/clam/file/search [post] -func (b *BaseApi) SearchClamFile(c *gin.Context) { - var req dto.ClamFileReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - content, err := clamService.LoadFile(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, content) -} - -// @Tags Clam -// @Summary Update clam file -// @Description 更新病毒扫描配置文件 -// @Accept json -// @Param request body dto.UpdateByNameAndFile true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /toolbox/clam/file/update [post] -func (b *BaseApi) UpdateFile(c *gin.Context) { - var req dto.UpdateByNameAndFile - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := clamService.UpdateFile(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags Clam -// @Summary Delete clam -// @Description 删除扫描规则 -// @Accept json -// @Param request body dto.ClamDelete true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /toolbox/clam/del [post] -// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"clams","output_column":"name","output_value":"names"}],"formatZH":"删除扫描规则 [names]","formatEN":"delete clam [names]"} -func (b *BaseApi) DeleteClam(c *gin.Context) { - var req dto.ClamDelete - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := clamService.Delete(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Clam -// @Summary Handle clam scan -// @Description 执行病毒扫描 -// @Accept json -// @Param request body dto.OperateByID true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /toolbox/clam/handle [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":true,"db":"clams","output_column":"name","output_value":"name"}],"formatZH":"执行病毒扫描 [name]","formatEN":"handle clam scan [name]"} -func (b *BaseApi) HandleClamScan(c *gin.Context) { - var req dto.OperateByID - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := clamService.HandleOnce(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} diff --git a/backend/app/api/v1/command.go b/backend/app/api/v1/command.go deleted file mode 100644 index 3c9926111..000000000 --- a/backend/app/api/v1/command.go +++ /dev/null @@ -1,223 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Command -// @Summary Create command -// @Description 创建快速命令 -// @Accept json -// @Param request body dto.CommandOperate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /hosts/command [post] -// @x-panel-log {"bodyKeys":["name","command"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建快捷命令 [name][command]","formatEN":"create quick command [name][command]"} -func (b *BaseApi) CreateCommand(c *gin.Context) { - var req dto.CommandOperate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := commandService.Create(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Redis Command -// @Summary Save redis command -// @Description 保存 Redis 快速命令 -// @Accept json -// @Param request body dto.RedisCommand true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /hosts/command/redis [post] -// @x-panel-log {"bodyKeys":["name","command"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"保存 redis 快捷命令 [name][command]","formatEN":"save quick command for redis [name][command]"} -func (b *BaseApi) SaveRedisCommand(c *gin.Context) { - var req dto.RedisCommand - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := commandService.SaveRedisCommand(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Command -// @Summary Page commands -// @Description 获取快速命令列表分页 -// @Accept json -// @Param request body dto.SearchWithPage true "request" -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /hosts/command/search [post] -func (b *BaseApi) SearchCommand(c *gin.Context) { - var req dto.SearchCommandWithPage - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - total, list, err := commandService.SearchWithPage(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - -// @Tags Redis Command -// @Summary Page redis commands -// @Description 获取 redis 快速命令列表分页 -// @Accept json -// @Param request body dto.SearchWithPage true "request" -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /hosts/command/redis/search [post] -func (b *BaseApi) SearchRedisCommand(c *gin.Context) { - var req dto.SearchWithPage - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - total, list, err := commandService.SearchRedisCommandWithPage(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - -// @Tags Command -// @Summary Tree commands -// @Description 获取快速命令树 -// @Accept json -// @Success 200 {Array} dto.CommandTree -// @Security ApiKeyAuth -// @Router /hosts/command/tree [get] -func (b *BaseApi) SearchCommandTree(c *gin.Context) { - list, err := commandService.SearchForTree() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, list) -} - -// @Tags Redis Command -// @Summary List redis commands -// @Description 获取 redis 快速命令列表 -// @Success 200 {Array} dto.RedisCommand -// @Security ApiKeyAuth -// @Router /hosts/command/redis [get] -func (b *BaseApi) ListRedisCommand(c *gin.Context) { - list, err := commandService.ListRedisCommand() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, list) -} - -// @Tags Command -// @Summary List commands -// @Description 获取快速命令列表 -// @Success 200 {object} dto.CommandInfo -// @Security ApiKeyAuth -// @Router /hosts/command [get] -func (b *BaseApi) ListCommand(c *gin.Context) { - list, err := commandService.List() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, list) -} - -// @Tags Command -// @Summary Delete command -// @Description 删除快速命令 -// @Accept json -// @Param request body dto.BatchDeleteReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /hosts/command/del [post] -// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"commands","output_column":"name","output_value":"names"}],"formatZH":"删除快捷命令 [names]","formatEN":"delete quick command [names]"} -func (b *BaseApi) DeleteCommand(c *gin.Context) { - var req dto.BatchDeleteReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := commandService.Delete(req.Ids); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Redis Command -// @Summary Delete redis command -// @Description 删除 redis 快速命令 -// @Accept json -// @Param request body dto.BatchDeleteReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /hosts/command/redis/del [post] -// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"redis_commands","output_column":"name","output_value":"names"}],"formatZH":"删除 redis 快捷命令 [names]","formatEN":"delete quick command of redis [names]"} -func (b *BaseApi) DeleteRedisCommand(c *gin.Context) { - var req dto.BatchDeleteReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := commandService.DeleteRedisCommand(req.Ids); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Command -// @Summary Update command -// @Description 更新快速命令 -// @Accept json -// @Param request body dto.CommandOperate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /hosts/command/update [post] -// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新快捷命令 [name]","formatEN":"update quick command [name]"} -func (b *BaseApi) UpdateCommand(c *gin.Context) { - var req dto.CommandOperate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - upMap := make(map[string]interface{}) - upMap["name"] = req.Name - upMap["group_id"] = req.GroupID - upMap["command"] = req.Command - if err := commandService.Update(req.ID, upMap); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} diff --git a/backend/app/api/v1/compose_template.go b/backend/app/api/v1/compose_template.go deleted file mode 100644 index 5fcba846d..000000000 --- a/backend/app/api/v1/compose_template.go +++ /dev/null @@ -1,121 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Container Compose-template -// @Summary Create compose template -// @Description 创建容器编排模版 -// @Accept json -// @Param request body dto.ComposeTemplateCreate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/template [post] -// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 compose 模版 [name]","formatEN":"create compose template [name]"} -func (b *BaseApi) CreateComposeTemplate(c *gin.Context) { - var req dto.ComposeTemplateCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := composeTemplateService.Create(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Container Compose-template -// @Summary Page compose templates -// @Description 获取容器编排模版列表分页 -// @Accept json -// @Param request body dto.SearchWithPage true "request" -// @Produce json -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /containers/template/search [post] -func (b *BaseApi) SearchComposeTemplate(c *gin.Context) { - var req dto.SearchWithPage - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - total, list, err := composeTemplateService.SearchWithPage(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - -// @Tags Container Compose-template -// @Summary List compose templates -// @Description 获取容器编排模版列表 -// @Produce json -// @Success 200 {array} dto.ComposeTemplateInfo -// @Security ApiKeyAuth -// @Router /containers/template [get] -func (b *BaseApi) ListComposeTemplate(c *gin.Context) { - list, err := composeTemplateService.List() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, list) -} - -// @Tags Container Compose-template -// @Summary Delete compose template -// @Description 删除容器编排模版 -// @Accept json -// @Param request body dto.BatchDelete true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/template/del [post] -// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"compose_templates","output_column":"name","output_value":"names"}],"formatZH":"删除 compose 模版 [names]","formatEN":"delete compose template [names]"} -func (b *BaseApi) DeleteComposeTemplate(c *gin.Context) { - var req dto.BatchDeleteReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := composeTemplateService.Delete(req.Ids); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Container Compose-template -// @Summary Update compose template -// @Description 更新容器编排模版 -// @Accept json -// @Param request body dto.ComposeTemplateUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/template/update [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"compose_templates","output_column":"name","output_value":"name"}],"formatZH":"更新 compose 模版 [name]","formatEN":"update compose template information [name]"} -func (b *BaseApi) UpdateComposeTemplate(c *gin.Context) { - var req dto.ComposeTemplateUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - upMap := make(map[string]interface{}) - upMap["content"] = req.Content - upMap["description"] = req.Description - if err := composeTemplateService.Update(req.ID, upMap); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} diff --git a/backend/app/api/v1/container.go b/backend/app/api/v1/container.go deleted file mode 100644 index 5c8213662..000000000 --- a/backend/app/api/v1/container.go +++ /dev/null @@ -1,699 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/gin-gonic/gin" - "github.com/pkg/errors" - "strconv" -) - -// @Tags Container -// @Summary Page containers -// @Description 获取容器列表分页 -// @Accept json -// @Param request body dto.PageContainer true "request" -// @Produce json -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /containers/search [post] -func (b *BaseApi) SearchContainer(c *gin.Context) { - var req dto.PageContainer - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - total, list, err := containerService.Page(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - -// @Tags Container -// @Summary List containers -// @Description 获取容器名称 -// @Accept json -// @Produce json -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/list [post] -func (b *BaseApi) ListContainer(c *gin.Context) { - list, err := containerService.List() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, list) -} - -// @Tags Container Compose -// @Summary Page composes -// @Description 获取编排列表分页 -// @Accept json -// @Param request body dto.SearchWithPage true "request" -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /containers/compose/search [post] -func (b *BaseApi) SearchCompose(c *gin.Context) { - var req dto.SearchWithPage - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - total, list, err := containerService.PageCompose(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - -// @Tags Container Compose -// @Summary Test compose -// @Description 测试 compose 是否可用 -// @Accept json -// @Param request body dto.ComposeCreate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/compose/test [post] -// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"检测 compose [name] 格式","formatEN":"check compose [name]"} -func (b *BaseApi) TestCompose(c *gin.Context) { - var req dto.ComposeCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - isOK, err := containerService.TestCompose(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, isOK) -} - -// @Tags Container Compose -// @Summary Create compose -// @Description 创建容器编排 -// @Accept json -// @Param request body dto.ComposeCreate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/compose [post] -// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 compose [name]","formatEN":"create compose [name]"} -func (b *BaseApi) CreateCompose(c *gin.Context) { - var req dto.ComposeCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - log, err := containerService.CreateCompose(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, log) -} - -// @Tags Container Compose -// @Summary Operate compose -// @Description 容器编排操作 -// @Accept json -// @Param request body dto.ComposeOperation true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/compose/operate [post] -// @x-panel-log {"bodyKeys":["name","operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"compose [operation] [name]","formatEN":"compose [operation] [name]"} -func (b *BaseApi) OperatorCompose(c *gin.Context) { - var req dto.ComposeOperation - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := containerService.ComposeOperation(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Container -// @Summary Update container -// @Description 更新容器 -// @Accept json -// @Param request body dto.ContainerOperate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/update [post] -// @x-panel-log {"bodyKeys":["name","image"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新容器 [name][image]","formatEN":"update container [name][image]"} -func (b *BaseApi) ContainerUpdate(c *gin.Context) { - var req dto.ContainerOperate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := containerService.ContainerUpdate(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Container -// @Summary Load container info -// @Description 获取容器表单信息 -// @Accept json -// @Param request body dto.OperationWithName true "request" -// @Success 200 {object} dto.ContainerOperate -// @Security ApiKeyAuth -// @Router /containers/info [post] -func (b *BaseApi) ContainerInfo(c *gin.Context) { - var req dto.OperationWithName - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - data, err := containerService.ContainerInfo(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, data) -} - -// @Summary Load container limits -// @Description 获取容器限制 -// @Success 200 {object} dto.ResourceLimit -// @Security ApiKeyAuth -// @Router /containers/limit [get] -func (b *BaseApi) LoadResourceLimit(c *gin.Context) { - data, err := containerService.LoadResourceLimit() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, data) -} - -// @Summary Load container stats -// @Description 获取容器列表资源占用 -// @Success 200 {array} dto.ContainerListStats -// @Security ApiKeyAuth -// @Router /containers/list/stats [get] -func (b *BaseApi) ContainerListStats(c *gin.Context) { - data, err := containerService.ContainerListStats() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, data) -} - -// @Tags Container -// @Summary Create container -// @Description 创建容器 -// @Accept json -// @Param request body dto.ContainerOperate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers [post] -// @x-panel-log {"bodyKeys":["name","image"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建容器 [name][image]","formatEN":"create container [name][image]"} -func (b *BaseApi) ContainerCreate(c *gin.Context) { - var req dto.ContainerOperate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := containerService.ContainerCreate(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Container -// @Summary Upgrade container -// @Description 更新容器镜像 -// @Accept json -// @Param request body dto.ContainerUpgrade true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/upgrade [post] -// @x-panel-log {"bodyKeys":["name","image"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新容器镜像 [name][image]","formatEN":"upgrade container image [name][image]"} -func (b *BaseApi) ContainerUpgrade(c *gin.Context) { - var req dto.ContainerUpgrade - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := containerService.ContainerUpgrade(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Container -// @Summary Clean container -// @Description 容器清理 -// @Accept json -// @Param request body dto.ContainerPrune true "request" -// @Success 200 {object} dto.ContainerPruneReport -// @Security ApiKeyAuth -// @Router /containers/prune [post] -// @x-panel-log {"bodyKeys":["pruneType"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清理容器 [pruneType]","formatEN":"clean container [pruneType]"} -func (b *BaseApi) ContainerPrune(c *gin.Context) { - var req dto.ContainerPrune - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - report, err := containerService.Prune(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, report) -} - -// @Tags Container -// @Summary Clean container log -// @Description 清理容器日志 -// @Accept json -// @Param request body dto.OperationWithName true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/clean/log [post] -// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清理容器 [name] 日志","formatEN":"clean container [name] logs"} -func (b *BaseApi) CleanContainerLog(c *gin.Context) { - var req dto.OperationWithName - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := containerService.ContainerLogClean(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Container -// @Summary Load container log -// @Description 获取容器操作日志 -// @Accept json -// @Param request body dto.OperationWithNameAndType true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/load/log [post] -func (b *BaseApi) LoadContainerLog(c *gin.Context) { - var req dto.OperationWithNameAndType - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - content := containerService.LoadContainerLogs(req) - helper.SuccessWithData(c, content) -} - -// @Tags Container -// @Summary Rename Container -// @Description 容器重命名 -// @Accept json -// @Param request body dto.ContainerRename true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/rename [post] -// @x-panel-log {"bodyKeys":["name","newName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"容器重命名 [name] => [newName]","formatEN":"rename container [name] => [newName]"} -func (b *BaseApi) ContainerRename(c *gin.Context) { - var req dto.ContainerRename - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := containerService.ContainerRename(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Container -// @Summary Commit Container -// @Description 容器提交生成新镜像 -// @Accept json -// @Param request body dto.ContainerCommit true "request" -// @Success 200 -// @Router /containers/commit [post] -func (b *BaseApi) ContainerCommit(c *gin.Context) { - var req dto.ContainerCommit - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := containerService.ContainerCommit(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Container -// @Summary Operate Container -// @Description 容器操作 -// @Accept json -// @Param request body dto.ContainerOperation true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/operate [post] -// @x-panel-log {"bodyKeys":["names","operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"容器 [names] 执行 [operation]","formatEN":"container [operation] [names]"} -func (b *BaseApi) ContainerOperation(c *gin.Context) { - var req dto.ContainerOperation - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := containerService.ContainerOperation(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Container -// @Summary Container stats -// @Description 容器监控信息 -// @Param id path integer true "容器id" -// @Success 200 {object} dto.ContainerStats -// @Security ApiKeyAuth -// @Router /containers/stats/:id [get] -func (b *BaseApi) ContainerStats(c *gin.Context) { - containerID, ok := c.Params.Get("id") - if !ok { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error container id in path")) - return - } - - result, err := containerService.ContainerStats(containerID) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, result) -} - -// @Tags Container -// @Summary Container inspect -// @Description 容器详情 -// @Accept json -// @Param request body dto.InspectReq true "request" -// @Success 200 {string} result -// @Security ApiKeyAuth -// @Router /containers/inspect [post] -func (b *BaseApi) Inspect(c *gin.Context) { - var req dto.InspectReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - result, err := containerService.Inspect(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, result) -} - -// @Tags Container -// @Summary Container logs -// @Description 容器日志 -// @Param container query string false "容器名称" -// @Param since query string false "时间筛选" -// @Param follow query string false "是否追踪" -// @Param tail query string false "显示行号" -// @Security ApiKeyAuth -// @Router /containers/search/log [post] -func (b *BaseApi) ContainerLogs(c *gin.Context) { - wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil) - if err != nil { - global.LOG.Errorf("gin context http handler failed, err: %v", err) - return - } - defer wsConn.Close() - - container := c.Query("container") - since := c.Query("since") - follow := c.Query("follow") == "true" - tail := c.Query("tail") - - if err := containerService.ContainerLogs(wsConn, "container", container, since, tail, follow); err != nil { - _ = wsConn.WriteMessage(1, []byte(err.Error())) - return - } -} - -// @Description 下载容器日志 -// @Router /containers/download/log [post] -func (b *BaseApi) DownloadContainerLogs(c *gin.Context) { - var req dto.ContainerLog - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - err := containerService.DownloadContainerLogs(req.ContainerType, req.Container, req.Since, strconv.Itoa(int(req.Tail)), c) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - } -} - -// @Tags Container Network -// @Summary Page networks -// @Description 获取容器网络列表分页 -// @Accept json -// @Param request body dto.SearchWithPage true "request" -// @Produce json -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /containers/network/search [post] -func (b *BaseApi) SearchNetwork(c *gin.Context) { - var req dto.SearchWithPage - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - total, list, err := containerService.PageNetwork(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - -// @Tags Container Network -// @Summary List networks -// @Description 获取容器网络列表 -// @Accept json -// @Produce json -// @Success 200 {array} dto.Options -// @Security ApiKeyAuth -// @Router /containers/network [get] -func (b *BaseApi) ListNetwork(c *gin.Context) { - list, err := containerService.ListNetwork() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, list) -} - -// @Tags Container Network -// @Summary Delete network -// @Description 删除容器网络 -// @Accept json -// @Param request body dto.BatchDelete true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/network/del [post] -// @x-panel-log {"bodyKeys":["names"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除容器网络 [names]","formatEN":"delete container network [names]"} -func (b *BaseApi) DeleteNetwork(c *gin.Context) { - var req dto.BatchDelete - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := containerService.DeleteNetwork(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Container Network -// @Summary Create network -// @Description 创建容器网络 -// @Accept json -// @Param request body dto.NetworkCreate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/network [post] -// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建容器网络 name","formatEN":"create container network [name]"} -func (b *BaseApi) CreateNetwork(c *gin.Context) { - var req dto.NetworkCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := containerService.CreateNetwork(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Container Volume -// @Summary Page volumes -// @Description 获取容器存储卷分页 -// @Accept json -// @Param request body dto.SearchWithPage true "request" -// @Produce json -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /containers/volume/search [post] -func (b *BaseApi) SearchVolume(c *gin.Context) { - var req dto.SearchWithPage - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - total, list, err := containerService.PageVolume(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - -// @Tags Container Volume -// @Summary List volumes -// @Description 获取容器存储卷列表 -// @Accept json -// @Produce json -// @Success 200 {array} dto.Options -// @Security ApiKeyAuth -// @Router /containers/volume [get] -func (b *BaseApi) ListVolume(c *gin.Context) { - list, err := containerService.ListVolume() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, list) -} - -// @Tags Container Volume -// @Summary Delete volume -// @Description 删除容器存储卷 -// @Accept json -// @Param request body dto.BatchDelete true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/volume/del [post] -// @x-panel-log {"bodyKeys":["names"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除容器存储卷 [names]","formatEN":"delete container volume [names]"} -func (b *BaseApi) DeleteVolume(c *gin.Context) { - var req dto.BatchDelete - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := containerService.DeleteVolume(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Container Volume -// @Summary Create volume -// @Description 创建容器存储卷 -// @Accept json -// @Param request body dto.VolumeCreate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/volume [post] -// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建容器存储卷 [name]","formatEN":"create container volume [name]"} -func (b *BaseApi) CreateVolume(c *gin.Context) { - var req dto.VolumeCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := containerService.CreateVolume(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Container Compose -// @Summary Update compose -// @Description 更新容器编排 -// @Accept json -// @Param request body dto.ComposeUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/compose/update [post] -// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 compose [name]","formatEN":"update compose information [name]"} -func (b *BaseApi) ComposeUpdate(c *gin.Context) { - var req dto.ComposeUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := containerService.ComposeUpdate(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Container Compose -// @Summary Container Compose logs -// @Description docker-compose 日志 -// @Param compose query string false "compose 文件地址" -// @Param since query string false "时间筛选" -// @Param follow query string false "是否追踪" -// @Param tail query string false "显示行号" -// @Security ApiKeyAuth -// @Router /containers/compose/search/log [get] -func (b *BaseApi) ComposeLogs(c *gin.Context) { - wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil) - if err != nil { - global.LOG.Errorf("gin context http handler failed, err: %v", err) - return - } - defer wsConn.Close() - - compose := c.Query("compose") - since := c.Query("since") - follow := c.Query("follow") == "true" - tail := c.Query("tail") - - if err := containerService.ContainerLogs(wsConn, "compose", compose, since, tail, follow); err != nil { - _ = wsConn.WriteMessage(1, []byte(err.Error())) - return - } -} diff --git a/backend/app/api/v1/cronjob.go b/backend/app/api/v1/cronjob.go deleted file mode 100644 index 181a28cd7..000000000 --- a/backend/app/api/v1/cronjob.go +++ /dev/null @@ -1,241 +0,0 @@ -package v1 - -import ( - "time" - - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/gin-gonic/gin" -) - -// @Tags Cronjob -// @Summary Create cronjob -// @Description 创建计划任务 -// @Accept json -// @Param request body dto.CronjobCreate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /cronjobs [post] -// @x-panel-log {"bodyKeys":["type","name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建计划任务 [type][name]","formatEN":"create cronjob [type][name]"} -func (b *BaseApi) CreateCronjob(c *gin.Context) { - var req dto.CronjobCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := cronjobService.Create(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Cronjob -// @Summary Page cronjobs -// @Description 获取计划任务分页 -// @Accept json -// @Param request body dto.PageCronjob true "request" -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /cronjobs/search [post] -func (b *BaseApi) SearchCronjob(c *gin.Context) { - var req dto.PageCronjob - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - total, list, err := cronjobService.SearchWithPage(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - -// @Tags Cronjob -// @Summary Page job records -// @Description 获取计划任务记录 -// @Accept json -// @Param request body dto.SearchRecord true "request" -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /cronjobs/search/records [post] -func (b *BaseApi) SearchJobRecords(c *gin.Context) { - var req dto.SearchRecord - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - loc, _ := time.LoadLocation(common.LoadTimeZone()) - req.StartTime = req.StartTime.In(loc) - req.EndTime = req.EndTime.In(loc) - - total, list, err := cronjobService.SearchRecords(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - -// @Tags Cronjob -// @Summary Load Cronjob record log -// @Description 获取计划任务记录日志 -// @Accept json -// @Param request body dto.OperateByID true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /cronjobs/records/log [post] -func (b *BaseApi) LoadRecordLog(c *gin.Context) { - var req dto.OperateByID - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - content := cronjobService.LoadRecordLog(req) - helper.SuccessWithData(c, content) -} - -// @Tags Cronjob -// @Summary Clean job records -// @Description 清空计划任务记录 -// @Accept json -// @Param request body dto.CronjobClean true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /cronjobs/records/clean [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"cronjobs","output_column":"name","output_value":"name"}],"formatZH":"清空计划任务记录 [name]","formatEN":"clean cronjob [name] records"} -func (b *BaseApi) CleanRecord(c *gin.Context) { - var req dto.CronjobClean - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := cronjobService.CleanRecord(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -// @Tags Cronjob -// @Summary Delete cronjob -// @Description 删除计划任务 -// @Accept json -// @Param request body dto.CronjobBatchDelete true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /cronjobs/del [post] -// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"cronjobs","output_column":"name","output_value":"names"}],"formatZH":"删除计划任务 [names]","formatEN":"delete cronjob [names]"} -func (b *BaseApi) DeleteCronjob(c *gin.Context) { - var req dto.CronjobBatchDelete - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := cronjobService.Delete(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Cronjob -// @Summary Update cronjob -// @Description 更新计划任务 -// @Accept json -// @Param request body dto.CronjobUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /cronjobs/update [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"cronjobs","output_column":"name","output_value":"name"}],"formatZH":"更新计划任务 [name]","formatEN":"update cronjob [name]"} -func (b *BaseApi) UpdateCronjob(c *gin.Context) { - var req dto.CronjobUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := cronjobService.Update(req.ID, req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Cronjob -// @Summary Update cronjob status -// @Description 更新计划任务状态 -// @Accept json -// @Param request body dto.CronjobUpdateStatus true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /cronjobs/status [post] -// @x-panel-log {"bodyKeys":["id","status"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"cronjobs","output_column":"name","output_value":"name"}],"formatZH":"修改计划任务 [name] 状态为 [status]","formatEN":"change the status of cronjob [name] to [status]."} -func (b *BaseApi) UpdateCronjobStatus(c *gin.Context) { - var req dto.CronjobUpdateStatus - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := cronjobService.UpdateStatus(req.ID, req.Status); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Cronjob -// @Summary Download cronjob records -// @Description 下载计划任务记录 -// @Accept json -// @Param request body dto.CronjobDownload true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /cronjobs/download [post] -// @x-panel-log {"bodyKeys":["recordID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"recordID","isList":false,"db":"job_records","output_column":"file","output_value":"file"}],"formatZH":"下载计划任务记录 [file]","formatEN":"download the cronjob record [file]"} -func (b *BaseApi) TargetDownload(c *gin.Context) { - var req dto.CronjobDownload - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - filePath, err := cronjobService.Download(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - c.File(filePath) -} - -// @Tags Cronjob -// @Summary Handle cronjob once -// @Description 手动执行计划任务 -// @Accept json -// @Param request body dto.OperateByID true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /cronjobs/handle [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"cronjobs","output_column":"name","output_value":"name"}],"formatZH":"手动执行计划任务 [name]","formatEN":"manually execute the cronjob [name]"} -func (b *BaseApi) HandleOnce(c *gin.Context) { - var req dto.OperateByID - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := cronjobService.HandleOnce(req.ID); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} diff --git a/backend/app/api/v1/dashboard.go b/backend/app/api/v1/dashboard.go deleted file mode 100644 index 0ada8bdb8..000000000 --- a/backend/app/api/v1/dashboard.go +++ /dev/null @@ -1,99 +0,0 @@ -package v1 - -import ( - "errors" - - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Dashboard -// @Summary Load os info -// @Description 获取服务器基础数据 -// @Accept json -// @Success 200 {object} dto.OsInfo -// @Security ApiKeyAuth -// @Router /dashboard/base/os [get] -func (b *BaseApi) LoadDashboardOsInfo(c *gin.Context) { - data, err := dashboardService.LoadOsInfo() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, data) -} - -// @Tags Dashboard -// @Summary Load dashboard base info -// @Description 获取首页基础数据 -// @Accept json -// @Param ioOption path string true "request" -// @Param netOption path string true "request" -// @Success 200 {object} dto.DashboardBase -// @Security ApiKeyAuth -// @Router /dashboard/base/:ioOption/:netOption [get] -func (b *BaseApi) LoadDashboardBaseInfo(c *gin.Context) { - ioOption, ok := c.Params.Get("ioOption") - if !ok { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error ioOption in path")) - return - } - netOption, ok := c.Params.Get("netOption") - if !ok { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error netOption in path")) - return - } - data, err := dashboardService.LoadBaseInfo(ioOption, netOption) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, data) -} - -// @Tags Dashboard -// @Summary Load dashboard current info -// @Description 获取首页实时数据 -// @Accept json -// @Param ioOption path string true "request" -// @Param netOption path string true "request" -// @Success 200 {object} dto.DashboardCurrent -// @Security ApiKeyAuth -// @Router /dashboard/current/:ioOption/:netOption [get] -func (b *BaseApi) LoadDashboardCurrentInfo(c *gin.Context) { - ioOption, ok := c.Params.Get("ioOption") - if !ok { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error ioOption in path")) - return - } - netOption, ok := c.Params.Get("netOption") - if !ok { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error netOption in path")) - return - } - - data := dashboardService.LoadCurrentInfo(ioOption, netOption) - helper.SuccessWithData(c, data) -} - -// @Tags Dashboard -// @Summary System restart -// @Description 重启服务器/面板 -// @Accept json -// @Param operation path string true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /dashboard/system/restart/:operation [post] -func (b *BaseApi) SystemRestart(c *gin.Context) { - operation, ok := c.Params.Get("operation") - if !ok { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error operation in path")) - return - } - if err := dashboardService.Restart(operation); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} diff --git a/backend/app/api/v1/database.go b/backend/app/api/v1/database.go deleted file mode 100644 index 5a198f7c1..000000000 --- a/backend/app/api/v1/database.go +++ /dev/null @@ -1,229 +0,0 @@ -package v1 - -import ( - "encoding/base64" - - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Database -// @Summary Create database -// @Description 创建远程数据库 -// @Accept json -// @Param request body dto.DatabaseCreate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/db [post] -// @x-panel-log {"bodyKeys":["name", "type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建远程数据库 [name][type]","formatEN":"create database [name][type]"} -func (b *BaseApi) CreateDatabase(c *gin.Context) { - var req dto.DatabaseCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if req.SSL { - key, _ := base64.StdEncoding.DecodeString(req.ClientKey) - req.ClientKey = string(key) - cert, _ := base64.StdEncoding.DecodeString(req.ClientCert) - req.ClientCert = string(cert) - ca, _ := base64.StdEncoding.DecodeString(req.RootCert) - req.RootCert = string(ca) - } - - if err := databaseService.Create(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Database -// @Summary Check database -// @Description 检测远程数据库连接性 -// @Accept json -// @Param request body dto.DatabaseCreate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/db/check [post] -// @x-panel-log {"bodyKeys":["name", "type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"检测远程数据库 [name][type] 连接性","formatEN":"check if database [name][type] is connectable"} -func (b *BaseApi) CheckDatabase(c *gin.Context) { - var req dto.DatabaseCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if req.SSL { - clientKey, _ := base64.StdEncoding.DecodeString(req.ClientKey) - req.ClientKey = string(clientKey) - clientCert, _ := base64.StdEncoding.DecodeString(req.ClientCert) - req.ClientCert = string(clientCert) - rootCert, _ := base64.StdEncoding.DecodeString(req.RootCert) - req.RootCert = string(rootCert) - } - - helper.SuccessWithData(c, databaseService.CheckDatabase(req)) -} - -// @Tags Database -// @Summary Page databases -// @Description 获取远程数据库列表分页 -// @Accept json -// @Param request body dto.DatabaseSearch true "request" -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /databases/db/search [post] -func (b *BaseApi) SearchDatabase(c *gin.Context) { - var req dto.DatabaseSearch - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - total, list, err := databaseService.SearchWithPage(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - -// @Tags Database -// @Summary List databases -// @Description 获取远程数据库列表 -// @Success 200 {array} dto.DatabaseOption -// @Security ApiKeyAuth -// @Router /databases/db/list/:type [get] -func (b *BaseApi) ListDatabase(c *gin.Context) { - dbType, err := helper.GetStrParamByKey(c, "type") - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - list, err := databaseService.List(dbType) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, list) -} - -// @Tags Database -// @Summary List databases -// @Description 获取数据库列表 -// @Success 200 {array} dto.DatabaseItem -// @Security ApiKeyAuth -// @Router /databases/db/item/:type [get] -func (b *BaseApi) LoadDatabaseItems(c *gin.Context) { - dbType, err := helper.GetStrParamByKey(c, "type") - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - list, err := databaseService.LoadItems(dbType) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, list) -} - -// @Tags Database -// @Summary Get databases -// @Description 获取远程数据库 -// @Success 200 {object} dto.DatabaseInfo -// @Security ApiKeyAuth -// @Router /databases/db/:name [get] -func (b *BaseApi) GetDatabase(c *gin.Context) { - name, err := helper.GetStrParamByKey(c, "name") - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - data, err := databaseService.Get(name) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, data) -} - -// @Tags Database -// @Summary Check before delete remote database -// @Description Mysql 远程数据库删除前检查 -// @Accept json -// @Param request body dto.OperateByID true "request" -// @Success 200 {array} string -// @Security ApiKeyAuth -// @Router /db/remote/del/check [post] -func (b *BaseApi) DeleteCheckDatabase(c *gin.Context) { - var req dto.OperateByID - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - apps, err := databaseService.DeleteCheck(req.ID) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, apps) -} - -// @Tags Database -// @Summary Delete database -// @Description 删除远程数据库 -// @Accept json -// @Param request body dto.DatabaseDelete true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/db/del [post] -// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"databases","output_column":"name","output_value":"names"}],"formatZH":"删除远程数据库 [names]","formatEN":"delete database [names]"} -func (b *BaseApi) DeleteDatabase(c *gin.Context) { - var req dto.DatabaseDelete - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := databaseService.Delete(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Database -// @Summary Update database -// @Description 更新远程数据库 -// @Accept json -// @Param request body dto.DatabaseUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/db/update [post] -// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新远程数据库 [name]","formatEN":"update database [name]"} -func (b *BaseApi) UpdateDatabase(c *gin.Context) { - var req dto.DatabaseUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if req.SSL { - cKey, _ := base64.StdEncoding.DecodeString(req.ClientKey) - req.ClientKey = string(cKey) - cCert, _ := base64.StdEncoding.DecodeString(req.ClientCert) - req.ClientCert = string(cCert) - ca, _ := base64.StdEncoding.DecodeString(req.RootCert) - req.RootCert = string(ca) - } - - if err := databaseService.Update(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} diff --git a/backend/app/api/v1/database_common.go b/backend/app/api/v1/database_common.go deleted file mode 100644 index 5ff57736e..000000000 --- a/backend/app/api/v1/database_common.go +++ /dev/null @@ -1,75 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Database Common -// @Summary Load base info -// @Description 获取数据库基础信息 -// @Accept json -// @Param request body dto.OperationWithNameAndType true "request" -// @Success 200 {object} dto.DBBaseInfo -// @Security ApiKeyAuth -// @Router /databases/common/info [post] -func (b *BaseApi) LoadDBBaseInfo(c *gin.Context) { - var req dto.OperationWithNameAndType - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - data, err := dbCommonService.LoadBaseInfo(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, data) -} - -// @Tags Database Common -// @Summary Load Database conf -// @Description 获取数据库配置文件 -// @Accept json -// @Param request body dto.OperationWithNameAndType true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/common/load/file [post] -func (b *BaseApi) LoadDBFile(c *gin.Context) { - var req dto.OperationWithNameAndType - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - content, err := dbCommonService.LoadDatabaseFile(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, content) -} - -// @Tags Database Common -// @Summary Update conf by upload file -// @Description 上传替换配置文件 -// @Accept json -// @Param request body dto.DBConfUpdateByFile true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/common/update/conf [post] -// @x-panel-log {"bodyKeys":["type","database"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 [type] 数据库 [database] 配置信息","formatEN":"update the [type] [database] database configuration information"} -func (b *BaseApi) UpdateDBConfByFile(c *gin.Context) { - var req dto.DBConfUpdateByFile - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := dbCommonService.UpdateConfByFile(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} diff --git a/backend/app/api/v1/database_mysql.go b/backend/app/api/v1/database_mysql.go deleted file mode 100644 index 1168dad49..000000000 --- a/backend/app/api/v1/database_mysql.go +++ /dev/null @@ -1,350 +0,0 @@ -package v1 - -import ( - "context" - "encoding/base64" - - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Database Mysql -// @Summary Create mysql database -// @Description 创建 mysql 数据库 -// @Accept json -// @Param request body dto.MysqlDBCreate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases [post] -// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 mysql 数据库 [name]","formatEN":"create mysql database [name]"} -func (b *BaseApi) CreateMysql(c *gin.Context) { - var req dto.MysqlDBCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if len(req.Password) != 0 { - password, err := base64.StdEncoding.DecodeString(req.Password) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - req.Password = string(password) - } - - if _, err := mysqlService.Create(context.Background(), req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Database Mysql -// @Summary Bind user of mysql database -// @Description 绑定 mysql 数据库用户 -// @Accept json -// @Param request body dto.BindUser true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/bind [post] -// @x-panel-log {"bodyKeys":["database", "username"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"绑定 mysql 数据库名 [database] [username]","formatEN":"bind mysql database [database] [username]"} -func (b *BaseApi) BindUser(c *gin.Context) { - var req dto.BindUser - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if len(req.Password) != 0 { - password, err := base64.StdEncoding.DecodeString(req.Password) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - req.Password = string(password) - } - - if err := mysqlService.BindUser(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Database Mysql -// @Summary Update mysql database description -// @Description 更新 mysql 数据库库描述信息 -// @Accept json -// @Param request body dto.UpdateDescription true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/description/update [post] -// @x-panel-log {"bodyKeys":["id","description"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mysqls","output_column":"name","output_value":"name"}],"formatZH":"mysql 数据库 [name] 描述信息修改 [description]","formatEN":"The description of the mysql database [name] is modified => [description]"} -func (b *BaseApi) UpdateMysqlDescription(c *gin.Context) { - var req dto.UpdateDescription - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := mysqlService.UpdateDescription(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Database Mysql -// @Summary Change mysql password -// @Description 修改 mysql 密码 -// @Accept json -// @Param request body dto.ChangeDBInfo true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/change/password [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mysqls","output_column":"name","output_value":"name"}],"formatZH":"更新数据库 [name] 密码","formatEN":"Update database [name] password"} -func (b *BaseApi) ChangeMysqlPassword(c *gin.Context) { - var req dto.ChangeDBInfo - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if len(req.Value) != 0 { - value, err := base64.StdEncoding.DecodeString(req.Value) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - req.Value = string(value) - } - - if err := mysqlService.ChangePassword(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Database Mysql -// @Summary Change mysql access -// @Description 修改 mysql 访问权限 -// @Accept json -// @Param request body dto.ChangeDBInfo true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/change/access [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mysqls","output_column":"name","output_value":"name"}],"formatZH":"更新数据库 [name] 访问权限","formatEN":"Update database [name] access"} -func (b *BaseApi) ChangeMysqlAccess(c *gin.Context) { - var req dto.ChangeDBInfo - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := mysqlService.ChangeAccess(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Database Mysql -// @Summary Update mysql variables -// @Description mysql 性能调优 -// @Accept json -// @Param request body dto.MysqlVariablesUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/variables/update [post] -// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整 mysql 数据库性能参数","formatEN":"adjust mysql database performance parameters"} -func (b *BaseApi) UpdateMysqlVariables(c *gin.Context) { - var req dto.MysqlVariablesUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := mysqlService.UpdateVariables(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Database Mysql -// @Summary Page mysql databases -// @Description 获取 mysql 数据库列表分页 -// @Accept json -// @Param request body dto.MysqlDBSearch true "request" -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /databases/search [post] -func (b *BaseApi) SearchMysql(c *gin.Context) { - var req dto.MysqlDBSearch - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - total, list, err := mysqlService.SearchWithPage(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - -// @Tags Database Mysql -// @Summary List mysql database names -// @Description 获取 mysql 数据库列表 -// @Accept json -// @Param request body dto.PageInfo true "request" -// @Success 200 {array} dto.MysqlOption -// @Security ApiKeyAuth -// @Router /databases/options [get] -func (b *BaseApi) ListDBName(c *gin.Context) { - list, err := mysqlService.ListDBOption() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, list) -} - -// @Tags Database Mysql -// @Summary Load mysql database from remote -// @Description 从服务器获取 -// @Accept json -// @Param request body dto.MysqlLoadDB true "request" -// @Security ApiKeyAuth -// @Router /databases/load [post] -func (b *BaseApi) LoadDBFromRemote(c *gin.Context) { - var req dto.MysqlLoadDB - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := mysqlService.LoadFromRemote(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -// @Tags Database Mysql -// @Summary Check before delete mysql database -// @Description Mysql 数据库删除前检查 -// @Accept json -// @Param request body dto.MysqlDBDeleteCheck true "request" -// @Success 200 {array} string -// @Security ApiKeyAuth -// @Router /databases/del/check [post] -func (b *BaseApi) DeleteCheckMysql(c *gin.Context) { - var req dto.MysqlDBDeleteCheck - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - apps, err := mysqlService.DeleteCheck(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, apps) -} - -// @Tags Database Mysql -// @Summary Delete mysql database -// @Description 删除 mysql 数据库 -// @Accept json -// @Param request body dto.MysqlDBDelete true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/del [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mysqls","output_column":"name","output_value":"name"}],"formatZH":"删除 mysql 数据库 [name]","formatEN":"delete mysql database [name]"} -func (b *BaseApi) DeleteMysql(c *gin.Context) { - var req dto.MysqlDBDelete - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - tx, ctx := helper.GetTxAndContext() - if err := mysqlService.Delete(ctx, req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - tx.Rollback() - return - } - tx.Commit() - helper.SuccessWithData(c, nil) -} - -// @Tags Database Mysql -// @Summary Load mysql remote access -// @Description 获取 mysql 远程访问权限 -// @Accept json -// @Param request body dto.OperationWithNameAndType true "request" -// @Success 200 {boolean} isRemote -// @Security ApiKeyAuth -// @Router /databases/remote [post] -func (b *BaseApi) LoadRemoteAccess(c *gin.Context) { - var req dto.OperationWithNameAndType - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - isRemote, err := mysqlService.LoadRemoteAccess(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, isRemote) -} - -// @Tags Database Mysql -// @Summary Load mysql status info -// @Description 获取 mysql 状态信息 -// @Accept json -// @Param request body dto.OperationWithNameAndType true "request" -// @Success 200 {object} dto.MysqlStatus -// @Security ApiKeyAuth -// @Router /databases/status [post] -func (b *BaseApi) LoadStatus(c *gin.Context) { - var req dto.OperationWithNameAndType - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - data, err := mysqlService.LoadStatus(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, data) -} - -// @Tags Database Mysql -// @Summary Load mysql variables info -// @Description 获取 mysql 性能参数信息 -// @Accept json -// @Param request body dto.OperationWithNameAndType true "request" -// @Success 200 {object} dto.MysqlVariables -// @Security ApiKeyAuth -// @Router /databases/variables [post] -func (b *BaseApi) LoadVariables(c *gin.Context) { - var req dto.OperationWithNameAndType - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - data, err := mysqlService.LoadVariables(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, data) -} diff --git a/backend/app/api/v1/database_postgresql.go b/backend/app/api/v1/database_postgresql.go deleted file mode 100644 index 5bd5f093a..000000000 --- a/backend/app/api/v1/database_postgresql.go +++ /dev/null @@ -1,233 +0,0 @@ -package v1 - -import ( - "context" - "encoding/base64" - - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Database Postgresql -// @Summary Create postgresql database -// @Description 创建 postgresql 数据库 -// @Accept json -// @Param request body dto.PostgresqlDBCreate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/pg [post] -// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 postgresql 数据库 [name]","formatEN":"create postgresql database [name]"} -func (b *BaseApi) CreatePostgresql(c *gin.Context) { - var req dto.PostgresqlDBCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if len(req.Password) != 0 { - password, err := base64.StdEncoding.DecodeString(req.Password) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - req.Password = string(password) - } - - if _, err := postgresqlService.Create(context.Background(), req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Database Postgresql -// @Summary Bind postgresql user -// @Description 绑定 postgresql 数据库用户 -// @Accept json -// @Param request body dto.PostgresqlBindUser true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/pg/bind [post] -// @x-panel-log {"bodyKeys":["name", "username"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"绑定 postgresql 数据库 [name] 用户 [username]","formatEN":"bind postgresql database [name] user [username]"} -func (b *BaseApi) BindPostgresqlUser(c *gin.Context) { - var req dto.PostgresqlBindUser - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := postgresqlService.BindUser(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Database Postgresql -// @Summary Update postgresql database description -// @Description 更新 postgresql 数据库库描述信息 -// @Accept json -// @Param request body dto.UpdateDescription true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/pg/description [post] -// @x-panel-log {"bodyKeys":["id","description"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_postgresqls","output_column":"name","output_value":"name"}],"formatZH":"postgresql 数据库 [name] 描述信息修改 [description]","formatEN":"The description of the postgresql database [name] is modified => [description]"} -func (b *BaseApi) UpdatePostgresqlDescription(c *gin.Context) { - var req dto.UpdateDescription - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := postgresqlService.UpdateDescription(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Database Postgresql -// @Summary Change postgresql privileges -// @Description 修改 postgresql 用户权限 -// @Accept json -// @Param request body dto.ChangeDBInfo true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/pg/privileges [post] -// @x-panel-log {"bodyKeys":["database", "username"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新数据库 [database] 用户 [username] 权限","formatEN":"Update [user] privileges of database [database]"} -func (b *BaseApi) ChangePostgresqlPrivileges(c *gin.Context) { - var req dto.PostgresqlPrivileges - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := postgresqlService.ChangePrivileges(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Database Postgresql -// @Summary Change postgresql password -// @Description 修改 postgresql 密码 -// @Accept json -// @Param request body dto.ChangeDBInfo true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/pg/password [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_postgresqls","output_column":"name","output_value":"name"}],"formatZH":"更新数据库 [name] 密码","formatEN":"Update database [name] password"} -func (b *BaseApi) ChangePostgresqlPassword(c *gin.Context) { - var req dto.ChangeDBInfo - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if len(req.Value) != 0 { - value, err := base64.StdEncoding.DecodeString(req.Value) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - req.Value = string(value) - } - - if err := postgresqlService.ChangePassword(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Database Postgresql -// @Summary Page postgresql databases -// @Description 获取 postgresql 数据库列表分页 -// @Accept json -// @Param request body dto.PostgresqlDBSearch true "request" -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /databases/pg/search [post] -func (b *BaseApi) SearchPostgresql(c *gin.Context) { - var req dto.PostgresqlDBSearch - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - total, list, err := postgresqlService.SearchWithPage(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - -// @Tags Database Postgresql -// @Summary Load postgresql database from remote -// @Description 从服务器获取 -// @Accept json -// @Param request body dto.PostgresqlLoadDB true "request" -// @Security ApiKeyAuth -// @Router /databases/pg/:database/load [post] -func (b *BaseApi) LoadPostgresqlDBFromRemote(c *gin.Context) { - database, err := helper.GetStrParamByKey(c, "database") - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - - if err := postgresqlService.LoadFromRemote(database); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -// @Tags Database Postgresql -// @Summary Check before delete postgresql database -// @Description Postgresql 数据库删除前检查 -// @Accept json -// @Param request body dto.PostgresqlDBDeleteCheck true "request" -// @Success 200 {array} string -// @Security ApiKeyAuth -// @Router /databases/pg/del/check [post] -func (b *BaseApi) DeleteCheckPostgresql(c *gin.Context) { - var req dto.PostgresqlDBDeleteCheck - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - apps, err := postgresqlService.DeleteCheck(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, apps) -} - -// @Tags Database Postgresql -// @Summary Delete postgresql database -// @Description 删除 postgresql 数据库 -// @Accept json -// @Param request body dto.PostgresqlDBDelete true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/pg/del [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_postgresqls","output_column":"name","output_value":"name"}],"formatZH":"删除 postgresql 数据库 [name]","formatEN":"delete postgresql database [name]"} -func (b *BaseApi) DeletePostgresql(c *gin.Context) { - var req dto.PostgresqlDBDelete - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - tx, ctx := helper.GetTxAndContext() - if err := postgresqlService.Delete(ctx, req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - tx.Rollback() - return - } - tx.Commit() - helper.SuccessWithData(c, nil) -} diff --git a/backend/app/api/v1/database_redis.go b/backend/app/api/v1/database_redis.go deleted file mode 100644 index 60b086280..000000000 --- a/backend/app/api/v1/database_redis.go +++ /dev/null @@ -1,170 +0,0 @@ -package v1 - -import ( - "encoding/base64" - - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Database Redis -// @Summary Load redis status info -// @Description 获取 redis 状态信息 -// @Accept json -// @Param request body dto.OperationWithName true "request" -// @Success 200 {object} dto.RedisStatus -// @Security ApiKeyAuth -// @Router /databases/redis/status [post] -func (b *BaseApi) LoadRedisStatus(c *gin.Context) { - var req dto.OperationWithName - if err := helper.CheckBind(&req, c); err != nil { - return - } - data, err := redisService.LoadStatus(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, data) -} - -// @Tags Database Redis -// @Summary Load redis conf -// @Description 获取 redis 配置信息 -// @Accept json -// @Param request body dto.OperationWithName true "request" -// @Success 200 {object} dto.RedisConf -// @Security ApiKeyAuth -// @Router /databases/redis/conf [post] -func (b *BaseApi) LoadRedisConf(c *gin.Context) { - var req dto.OperationWithName - if err := helper.CheckBind(&req, c); err != nil { - return - } - data, err := redisService.LoadConf(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, data) -} - -// @Tags Database Redis -// @Summary Load redis persistence conf -// @Description 获取 redis 持久化配置 -// @Accept json -// @Param request body dto.OperationWithName true "request" -// @Success 200 {object} dto.RedisPersistence -// @Security ApiKeyAuth -// @Router /databases/redis/persistence/conf [post] -func (b *BaseApi) LoadPersistenceConf(c *gin.Context) { - var req dto.OperationWithName - if err := helper.CheckBind(&req, c); err != nil { - return - } - data, err := redisService.LoadPersistenceConf(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, data) -} - -func (b *BaseApi) CheckHasCli(c *gin.Context) { - helper.SuccessWithData(c, redisService.CheckHasCli()) -} - -// @Tags Database Redis -// @Summary Install redis-cli -// @Description 安装 redis cli -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/redis/install/cli [post] -func (b *BaseApi) InstallCli(c *gin.Context) { - if err := redisService.InstallCli(); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithOutData(c) -} - -// @Tags Database Redis -// @Summary Update redis conf -// @Description 更新 redis 配置信息 -// @Accept json -// @Param request body dto.RedisConfUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/redis/conf/update [post] -// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 redis 数据库配置信息","formatEN":"update the redis database configuration information"} -func (b *BaseApi) UpdateRedisConf(c *gin.Context) { - var req dto.RedisConfUpdate - if err := helper.CheckBind(&req, c); err != nil { - return - } - - if err := redisService.UpdateConf(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Database Redis -// @Summary Change redis password -// @Description 更新 redis 密码 -// @Accept json -// @Param request body dto.ChangeRedisPass true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/redis/password [post] -// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 redis 数据库密码","formatEN":"change the password of the redis database"} -func (b *BaseApi) ChangeRedisPassword(c *gin.Context) { - var req dto.ChangeRedisPass - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if len(req.Value) != 0 { - value, err := base64.StdEncoding.DecodeString(req.Value) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - req.Value = string(value) - } - - if err := redisService.ChangePassword(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Database Redis -// @Summary Update redis persistence conf -// @Description 更新 redis 持久化配置 -// @Accept json -// @Param request body dto.RedisConfPersistenceUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /databases/redis/persistence/update [post] -// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"redis 数据库持久化配置更新","formatEN":"redis database persistence configuration update"} -func (b *BaseApi) UpdateRedisPersistenceConf(c *gin.Context) { - var req dto.RedisConfPersistenceUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := redisService.UpdatePersistenceConf(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} diff --git a/backend/app/api/v1/device.go b/backend/app/api/v1/device.go deleted file mode 100644 index 4b04e3f5d..000000000 --- a/backend/app/api/v1/device.go +++ /dev/null @@ -1,236 +0,0 @@ -package v1 - -import ( - "encoding/base64" - - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Device -// @Summary Load device base info -// @Description 获取设备基础信息 -// @Success 200 {object} dto.DeviceBaseInfo -// @Security ApiKeyAuth -// @Router /toolbox/device/base [post] -func (b *BaseApi) LoadDeviceBaseInfo(c *gin.Context) { - data, err := deviceService.LoadBaseInfo() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, data) -} - -// @Tags Device -// @Summary list time zone options -// @Description 获取系统可用时区选项 -// @Accept json -// @Success 200 {Array} string -// @Security ApiKeyAuth -// @Router /toolbox/device/zone/options [get] -func (b *BaseApi) LoadTimeOption(c *gin.Context) { - list, err := deviceService.LoadTimeZone() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, list) -} - -// @Tags Device -// @Summary load conf -// @Description 获取系统配置文件 -// @Accept json -// @Param request body dto.OperationWithName true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /toolbox/device/conf [post] -func (b *BaseApi) LoadDeviceConf(c *gin.Context) { - var req dto.OperationWithName - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - list, err := deviceService.LoadConf(req.Name) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, list) -} - -// @Tags Device -// @Summary Update device conf by file -// @Description 通过文件修改配置 -// @Accept json -// @Param request body dto.UpdateByNameAndFile true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /toolbox/device/update/byconf [post] -func (b *BaseApi) UpdateDeviceByFile(c *gin.Context) { - var req dto.UpdateByNameAndFile - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := deviceService.UpdateByConf(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -// @Tags Device -// @Summary Update device -// @Description 修改系统参数 -// @Accept json -// @Param request body dto.SettingUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /toolbox/device/update/conf [post] -// @x-panel-log {"bodyKeys":["key","value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改主机参数 [key] => [value]","formatEN":"update device conf [key] => [value]"} -func (b *BaseApi) UpdateDeviceConf(c *gin.Context) { - var req dto.SettingUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := deviceService.Update(req.Key, req.Value); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -// @Tags Device -// @Summary Update device hosts -// @Description 修改系统 hosts -// @Success 200 -// @Security ApiKeyAuth -// @Router /toolbox/device/update/host [post] -// @x-panel-log {"bodyKeys":["key","value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改主机 Host [key] => [value]","formatEN":"update device host [key] => [value]"} -func (b *BaseApi) UpdateDeviceHost(c *gin.Context) { - var req []dto.HostHelper - if err := helper.CheckBind(&req, c); err != nil { - return - } - - if err := deviceService.UpdateHosts(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -// @Tags Device -// @Summary Update device passwd -// @Description 修改系统密码 -// @Accept json -// @Param request body dto.ChangePasswd true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /toolbox/device/update/passwd [post] -func (b *BaseApi) UpdateDevicePasswd(c *gin.Context) { - var req dto.ChangePasswd - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if len(req.Passwd) != 0 { - password, err := base64.StdEncoding.DecodeString(req.Passwd) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - req.Passwd = string(password) - } - if err := deviceService.UpdatePasswd(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -// @Tags Device -// @Summary Update device swap -// @Description 修改系统 Swap -// @Accept json -// @Param request body dto.SwapHelper true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /toolbox/device/update/swap [post] -// @x-panel-log {"bodyKeys":["operate","path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 主机 swap [path]","formatEN":"[operate] device swap [path]"} -func (b *BaseApi) UpdateDeviceSwap(c *gin.Context) { - var req dto.SwapHelper - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := deviceService.UpdateSwap(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -// @Tags Device -// @Summary Check device DNS conf -// @Description 检查系统 DNS 配置可用性 -// @Accept json -// @Param request body dto.SettingUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /toolbox/device/check/dns [post] -func (b *BaseApi) CheckDNS(c *gin.Context) { - var req dto.SettingUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - data, err := deviceService.CheckDNS(req.Key, req.Value) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, data) -} - -// @Tags Device -// @Summary Scan system -// @Description 扫描系统垃圾文件 -// @Success 200 -// @Security ApiKeyAuth -// @Router /toolbox/scan [post] -// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"扫描系统垃圾文件","formatEN":"scan System Junk Files"} -func (b *BaseApi) ScanSystem(c *gin.Context) { - helper.SuccessWithData(c, deviceService.Scan()) -} - -// @Tags Device -// @Summary Clean system -// @Description 清理系统垃圾文件 -// @Accept json -// @Param request body []dto.Clean true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /toolbox/clean [post] -// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清理系统垃圾文件","formatEN":"Clean system junk files"} -func (b *BaseApi) SystemClean(c *gin.Context) { - var req []dto.Clean - if err := helper.CheckBind(&req, c); err != nil { - return - } - - deviceService.Clean(req) - - helper.SuccessWithData(c, nil) -} diff --git a/backend/app/api/v1/docker.go b/backend/app/api/v1/docker.go deleted file mode 100644 index 79a2e232f..000000000 --- a/backend/app/api/v1/docker.go +++ /dev/null @@ -1,169 +0,0 @@ -package v1 - -import ( - "os" - - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Container Docker -// @Summary Load docker status -// @Description 获取 docker 服务状态 -// @Produce json -// @Success 200 {string} status -// @Security ApiKeyAuth -// @Router /containers/docker/status [get] -func (b *BaseApi) LoadDockerStatus(c *gin.Context) { - status := dockerService.LoadDockerStatus() - helper.SuccessWithData(c, status) -} - -// @Tags Container Docker -// @Summary Load docker daemon.json -// @Description 获取 docker 配置信息(表单) -// @Produce json -// @Success 200 {object} string -// @Security ApiKeyAuth -// @Router /containers/daemonjson/file [get] -func (b *BaseApi) LoadDaemonJsonFile(c *gin.Context) { - if _, err := os.Stat(constant.DaemonJsonPath); err != nil { - helper.SuccessWithData(c, "daemon.json is not find in path") - return - } - content, err := os.ReadFile(constant.DaemonJsonPath) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, string(content)) -} - -// @Tags Container Docker -// @Summary Load docker daemon.json -// @Description 获取 docker 配置信息 -// @Produce json -// @Success 200 {object} dto.DaemonJsonConf -// @Security ApiKeyAuth -// @Router /containers/daemonjson [get] -func (b *BaseApi) LoadDaemonJson(c *gin.Context) { - conf := dockerService.LoadDockerConf() - helper.SuccessWithData(c, conf) -} - -// @Tags Container Docker -// @Summary Update docker daemon.json -// @Description 修改 docker 配置信息 -// @Accept json -// @Param request body dto.SettingUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/daemonjson/update [post] -// @x-panel-log {"bodyKeys":["key", "value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新配置 [key]","formatEN":"Updated configuration [key]"} -func (b *BaseApi) UpdateDaemonJson(c *gin.Context) { - var req dto.SettingUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := dockerService.UpdateConf(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -// @Tags Container Docker -// @Summary Update docker daemon.json log option -// @Description 修改 docker 日志配置 -// @Accept json -// @Param request body dto.LogOption true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/logoption/update [post] -// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新日志配置","formatEN":"Updated the log option"} -func (b *BaseApi) UpdateLogOption(c *gin.Context) { - var req dto.LogOption - if err := helper.CheckBind(&req, c); err != nil { - return - } - - if err := dockerService.UpdateLogOption(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -// @Tags Container Docker -// @Summary Update docker daemon.json ipv6 option -// @Description 修改 docker ipv6 配置 -// @Accept json -// @Param request body dto.LogOption true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/ipv6option/update [post] -// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 ipv6 配置","formatEN":"Updated the ipv6 option"} -func (b *BaseApi) UpdateIpv6Option(c *gin.Context) { - var req dto.Ipv6Option - if err := helper.CheckBind(&req, c); err != nil { - return - } - - if err := dockerService.UpdateIpv6Option(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -// @Tags Container Docker -// @Summary Update docker daemon.json by upload file -// @Description 上传替换 docker 配置文件 -// @Accept json -// @Param request body dto.DaemonJsonUpdateByFile true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/daemonjson/update/byfile [post] -// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新配置文件","formatEN":"Updated configuration file"} -func (b *BaseApi) UpdateDaemonJsonByFile(c *gin.Context) { - var req dto.DaemonJsonUpdateByFile - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := dockerService.UpdateConfByFile(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -// @Tags Container Docker -// @Summary Operate docker -// @Description Docker 操作 -// @Accept json -// @Param request body dto.DockerOperation true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/docker/operate [post] -// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"docker 服务 [operation]","formatEN":"[operation] docker service"} -func (b *BaseApi) OperateDocker(c *gin.Context) { - var req dto.DockerOperation - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := dockerService.OperateDocker(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} diff --git a/backend/app/api/v1/entry.go b/backend/app/api/v1/entry.go deleted file mode 100644 index dfec388d8..000000000 --- a/backend/app/api/v1/entry.go +++ /dev/null @@ -1,69 +0,0 @@ -package v1 - -import "github.com/1Panel-dev/1Panel/backend/app/service" - -type ApiGroup struct { - BaseApi -} - -var ApiGroupApp = new(ApiGroup) - -type BaseApi struct{} - -var ( - dashboardService = service.NewIDashboardService() - - appService = service.NewIAppService() - appInstallService = service.NewIAppInstalledService() - - containerService = service.NewIContainerService() - composeTemplateService = service.NewIComposeTemplateService() - imageRepoService = service.NewIImageRepoService() - imageService = service.NewIImageService() - dockerService = service.NewIDockerService() - - dbCommonService = service.NewIDBCommonService() - mysqlService = service.NewIMysqlService() - postgresqlService = service.NewIPostgresqlService() - databaseService = service.NewIDatabaseService() - redisService = service.NewIRedisService() - - cronjobService = service.NewICronjobService() - - hostService = service.NewIHostService() - groupService = service.NewIGroupService() - fileService = service.NewIFileService() - sshService = service.NewISSHService() - firewallService = service.NewIFirewallService() - - deviceService = service.NewIDeviceService() - fail2banService = service.NewIFail2BanService() - ftpService = service.NewIFtpService() - clamService = service.NewIClamService() - - settingService = service.NewISettingService() - backupService = service.NewIBackupService() - - commandService = service.NewICommandService() - - websiteService = service.NewIWebsiteService() - websiteDnsAccountService = service.NewIWebsiteDnsAccountService() - websiteSSLService = service.NewIWebsiteSSLService() - websiteAcmeAccountService = service.NewIWebsiteAcmeAccountService() - - nginxService = service.NewINginxService() - - logService = service.NewILogService() - snapshotService = service.NewISnapshotService() - - runtimeService = service.NewRuntimeService() - processService = service.NewIProcessService() - phpExtensionsService = service.NewIPHPExtensionsService() - - hostToolService = service.NewIHostToolService() - - recycleBinService = service.NewIRecycleBinService() - favoriteService = service.NewIFavoriteService() - - websiteCAService = service.NewIWebsiteCAService() -) diff --git a/backend/app/api/v1/fail2ban.go b/backend/app/api/v1/fail2ban.go deleted file mode 100644 index 5ffd2c3dc..000000000 --- a/backend/app/api/v1/fail2ban.go +++ /dev/null @@ -1,153 +0,0 @@ -package v1 - -import ( - "os" - - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Fail2ban -// @Summary Load fail2ban base info -// @Description 获取 Fail2ban 基础信息 -// @Success 200 {object} dto.Fail2BanBaseInfo -// @Security ApiKeyAuth -// @Router /toolbox/fail2ban/base [get] -func (b *BaseApi) LoadFail2BanBaseInfo(c *gin.Context) { - data, err := fail2banService.LoadBaseInfo() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, data) -} - -// @Tags Fail2ban -// @Summary Page fail2ban ip list -// @Description 获取 Fail2ban ip -// @Accept json -// @Param request body dto.Fail2BanSearch true "request" -// @Success 200 {Array} string -// @Security ApiKeyAuth -// @Router /toolbox/fail2ban/search [post] -func (b *BaseApi) SearchFail2Ban(c *gin.Context) { - var req dto.Fail2BanSearch - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - list, err := fail2banService.Search(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, list) -} - -// @Tags Fail2ban -// @Summary Operate fail2ban -// @Description 修改 Fail2ban 状态 -// @Accept json -// @Param request body dto.Operate true "request" -// @Security ApiKeyAuth -// @Router /toolbox/fail2ban/operate [post] -// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] Fail2ban","formatEN":"[operation] Fail2ban"} -func (b *BaseApi) OperateFail2Ban(c *gin.Context) { - var req dto.Operate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := fail2banService.Operate(req.Operation); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -// @Tags Fail2ban -// @Summary Operate sshd of fail2ban -// @Description 配置 sshd -// @Accept json -// @Param request body dto.Operate true "request" -// @Security ApiKeyAuth -// @Router /toolbox/fail2ban/operate/sshd [post] -func (b *BaseApi) OperateSSHD(c *gin.Context) { - var req dto.Fail2BanSet - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := fail2banService.OperateSSHD(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -// @Tags Fail2ban -// @Summary Update fail2ban conf -// @Description 修改 Fail2ban 配置 -// @Accept json -// @Param request body dto.Fail2BanUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /toolbox/fail2ban/update [post] -// @x-panel-log {"bodyKeys":["key","value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 Fail2ban 配置 [key] => [value]","formatEN":"update fail2ban conf [key] => [value]"} -func (b *BaseApi) UpdateFail2BanConf(c *gin.Context) { - var req dto.Fail2BanUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := fail2banService.UpdateConf(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Fail2ban -// @Summary Load fail2ban conf -// @Description 获取 fail2ban 配置文件 -// @Accept json -// @Success 200 -// @Security ApiKeyAuth -// @Router /toolbox/fail2ban/load/conf [get] -func (b *BaseApi) LoadFail2BanConf(c *gin.Context) { - path := "/etc/fail2ban/jail.local" - file, err := os.ReadFile(path) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, string(file)) -} - -// @Tags Fail2ban -// @Summary Update fail2ban conf by file -// @Description 通过文件修改 fail2ban 配置 -// @Accept json -// @Param request body dto.UpdateByFile true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /toolbox/fail2ban/update/byconf [post] -func (b *BaseApi) UpdateFail2BanConfByFile(c *gin.Context) { - var req dto.UpdateByFile - if err := helper.CheckBind(&req, c); err != nil { - return - } - if err := fail2banService.UpdateConfByFile(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} diff --git a/backend/app/api/v1/favorite.go b/backend/app/api/v1/favorite.go deleted file mode 100644 index 5cd1d57fe..000000000 --- a/backend/app/api/v1/favorite.go +++ /dev/null @@ -1,76 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags File -// @Summary List favorites -// @Description 获取收藏列表 -// @Accept json -// @Param request body dto.PageInfo true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/favorite/search [post] -func (b *BaseApi) SearchFavorite(c *gin.Context) { - var req dto.PageInfo - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - total, list, err := favoriteService.Page(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, dto.PageResult{ - Total: total, - Items: list, - }) -} - -// @Tags File -// @Summary Create favorite -// @Description 创建收藏 -// @Accept json -// @Param request body request.FavoriteCreate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/favorite [post] -// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"收藏文件/文件夹 [path]","formatEN":"收藏文件/文件夹 [path]"} -func (b *BaseApi) CreateFavorite(c *gin.Context) { - var req request.FavoriteCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - favorite, err := favoriteService.Create(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, favorite) -} - -// @Tags File -// @Summary Delete favorite -// @Description 删除收藏 -// @Accept json -// @Param request body request.FavoriteDelete true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/favorite/del [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"favorites","output_column":"path","output_value":"path"}],"formatZH":"删除收藏 [path]","formatEN":"delete avorite [path]"} -func (b *BaseApi) DeleteFavorite(c *gin.Context) { - var req request.FavoriteDelete - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := favoriteService.Delete(req.ID); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} diff --git a/backend/app/api/v1/file.go b/backend/app/api/v1/file.go deleted file mode 100644 index 19b44eb57..000000000 --- a/backend/app/api/v1/file.go +++ /dev/null @@ -1,801 +0,0 @@ -package v1 - -import ( - "errors" - "fmt" - "io" - "net/http" - "net/url" - "os" - "path" - "path/filepath" - "strconv" - "strings" - - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/app/dto/response" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/files" - websocket2 "github.com/1Panel-dev/1Panel/backend/utils/websocket" - "github.com/gin-gonic/gin" - "github.com/gorilla/websocket" -) - -// @Tags File -// @Summary List files -// @Description 获取文件列表 -// @Accept json -// @Param request body request.FileOption true "request" -// @Success 200 {object} response.FileInfo -// @Security ApiKeyAuth -// @Router /files/search [post] -func (b *BaseApi) ListFiles(c *gin.Context) { - var req request.FileOption - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - files, err := fileService.GetFileList(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, files) -} - -// @Tags File -// @Summary Page file -// @Description 分页获取上传文件 -// @Accept json -// @Param request body request.SearchUploadWithPage true "request" -// @Success 200 {array} response.FileInfo -// @Security ApiKeyAuth -// @Router /files/upload/search [post] -func (b *BaseApi) SearchUploadWithPage(c *gin.Context) { - var req request.SearchUploadWithPage - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - total, files, err := fileService.SearchUploadWithPage(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, dto.PageResult{ - Items: files, - Total: total, - }) -} - -// @Tags File -// @Summary Load files tree -// @Description 加载文件树 -// @Accept json -// @Param request body request.FileOption true "request" -// @Success 200 {array} response.FileTree -// @Security ApiKeyAuth -// @Router /files/tree [post] -func (b *BaseApi) GetFileTree(c *gin.Context) { - var req request.FileOption - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - tree, err := fileService.GetFileTree(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, tree) -} - -// @Tags File -// @Summary Create file -// @Description 创建文件/文件夹 -// @Accept json -// @Param request body request.FileCreate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /files [post] -// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建文件/文件夹 [path]","formatEN":"Create dir or file [path]"} -func (b *BaseApi) CreateFile(c *gin.Context) { - var req request.FileCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - err := fileService.Create(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags File -// @Summary Delete file -// @Description 删除文件/文件夹 -// @Accept json -// @Param request body request.FileDelete true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/del [post] -// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除文件/文件夹 [path]","formatEN":"Delete dir or file [path]"} -func (b *BaseApi) DeleteFile(c *gin.Context) { - var req request.FileDelete - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - err := fileService.Delete(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags File -// @Summary Batch delete file -// @Description 批量删除文件/文件夹 -// @Accept json -// @Param request body request.FileBatchDelete true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/batch/del [post] -// @x-panel-log {"bodyKeys":["paths"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"批量删除文件/文件夹 [paths]","formatEN":"Batch delete dir or file [paths]"} -func (b *BaseApi) BatchDeleteFile(c *gin.Context) { - var req request.FileBatchDelete - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - err := fileService.BatchDelete(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags File -// @Summary Change file mode -// @Description 修改文件权限 -// @Accept json -// @Param request body request.FileCreate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/mode [post] -// @x-panel-log {"bodyKeys":["path","mode"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改权限 [paths] => [mode]","formatEN":"Change mode [paths] => [mode]"} -func (b *BaseApi) ChangeFileMode(c *gin.Context) { - var req request.FileCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - err := fileService.ChangeMode(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags File -// @Summary Change file owner -// @Description 修改文件用户/组 -// @Accept json -// @Param request body request.FileRoleUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/owner [post] -// @x-panel-log {"bodyKeys":["path","user","group"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改用户/组 [paths] => [user]/[group]","formatEN":"Change owner [paths] => [user]/[group]"} -func (b *BaseApi) ChangeFileOwner(c *gin.Context) { - var req request.FileRoleUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := fileService.ChangeOwner(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags File -// @Summary Compress file -// @Description 压缩文件 -// @Accept json -// @Param request body request.FileCompress true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/compress [post] -// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"压缩文件 [name]","formatEN":"Compress file [name]"} -func (b *BaseApi) CompressFile(c *gin.Context) { - var req request.FileCompress - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - err := fileService.Compress(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags File -// @Summary Decompress file -// @Description 解压文件 -// @Accept json -// @Param request body request.FileDeCompress true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/decompress [post] -// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"解压 [path]","formatEN":"Decompress file [path]"} -func (b *BaseApi) DeCompressFile(c *gin.Context) { - var req request.FileDeCompress - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - err := fileService.DeCompress(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags File -// @Summary Load file content -// @Description 获取文件内容 -// @Accept json -// @Param request body request.FileContentReq true "request" -// @Success 200 {object} response.FileInfo -// @Security ApiKeyAuth -// @Router /files/content [post] -// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"获取文件内容 [path]","formatEN":"Load file content [path]"} -func (b *BaseApi) GetContent(c *gin.Context) { - var req request.FileContentReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - info, err := fileService.GetContent(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, info) -} - -// @Tags File -// @Summary Update file content -// @Description 更新文件内容 -// @Accept json -// @Param request body request.FileEdit true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/save [post] -// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新文件内容 [path]","formatEN":"Update file content [path]"} -func (b *BaseApi) SaveContent(c *gin.Context) { - var req request.FileEdit - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := fileService.SaveContent(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags File -// @Summary Upload file -// @Description 上传文件 -// @Param file formData file true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/upload [post] -// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"上传文件 [path]","formatEN":"Upload file [path]"} -func (b *BaseApi) UploadFiles(c *gin.Context) { - form, err := c.MultipartForm() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - uploadFiles := form.File["file"] - paths := form.Value["path"] - - overwrite := true - if ow, ok := form.Value["overwrite"]; ok { - if len(ow) != 0 { - parseBool, _ := strconv.ParseBool(ow[0]) - overwrite = parseBool - } - } - - if len(paths) == 0 || !strings.Contains(paths[0], "/") { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error paths in request")) - return - } - dir := path.Dir(paths[0]) - - _, err = os.Stat(dir) - if err != nil && os.IsNotExist(err) { - mode, err := files.GetParentMode(dir) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - if err = os.MkdirAll(dir, mode); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, fmt.Errorf("mkdir %s failed, err: %v", dir, err)) - return - } - } - info, err := os.Stat(dir) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - mode := info.Mode() - - fileOp := files.NewFileOp() - - success := 0 - failures := make(buserr.MultiErr) - for _, file := range uploadFiles { - dstFilename := path.Join(paths[0], file.Filename) - dstDir := path.Dir(dstFilename) - if !fileOp.Stat(dstDir) { - if err = fileOp.CreateDir(dstDir, mode); err != nil { - e := fmt.Errorf("create dir [%s] failed, err: %v", path.Dir(dstFilename), err) - failures[file.Filename] = e - global.LOG.Error(e) - continue - } - } - tmpFilename := dstFilename + ".tmp" - if err := c.SaveUploadedFile(file, tmpFilename); err != nil { - _ = os.Remove(tmpFilename) - e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err) - failures[file.Filename] = e - global.LOG.Error(e) - continue - } - dstInfo, statErr := os.Stat(dstFilename) - if overwrite { - _ = os.Remove(dstFilename) - } - - err = os.Rename(tmpFilename, dstFilename) - if err != nil { - _ = os.Remove(tmpFilename) - e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err) - failures[file.Filename] = e - global.LOG.Error(e) - continue - } - if statErr == nil { - _ = os.Chmod(dstFilename, dstInfo.Mode()) - } else { - _ = os.Chmod(dstFilename, mode) - } - success++ - } - if success == 0 { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, failures) - } else { - helper.SuccessWithMsg(c, fmt.Sprintf("%d files upload success", success)) - } -} - -// @Tags File -// @Summary Check file exist -// @Description 检测文件是否存在 -// @Accept json -// @Param request body request.FilePathCheck true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/check [post] -func (b *BaseApi) CheckFile(c *gin.Context) { - var req request.FilePathCheck - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if _, err := os.Stat(req.Path); err != nil { - helper.SuccessWithData(c, false) - return - } - helper.SuccessWithData(c, true) -} - -// @Tags File -// @Summary Change file name -// @Description 修改文件名称 -// @Accept json -// @Param request body request.FileRename true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/rename [post] -// @x-panel-log {"bodyKeys":["oldName","newName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"重命名 [oldName] => [newName]","formatEN":"Rename [oldName] => [newName]"} -func (b *BaseApi) ChangeFileName(c *gin.Context) { - var req request.FileRename - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := fileService.ChangeName(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags File -// @Summary Wget file -// @Description 下载远端文件 -// @Accept json -// @Param request body request.FileWget true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/wget [post] -// @x-panel-log {"bodyKeys":["url","path","name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"下载 url => [path]/[name]","formatEN":"Download url => [path]/[name]"} -func (b *BaseApi) WgetFile(c *gin.Context) { - var req request.FileWget - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - key, err := fileService.Wget(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, response.FileWgetRes{ - Key: key, - }) -} - -// @Tags File -// @Summary Move file -// @Description 移动文件 -// @Accept json -// @Param request body request.FileMove true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/move [post] -// @x-panel-log {"bodyKeys":["oldPaths","newPath"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"移动文件 [oldPaths] => [newPath]","formatEN":"Move [oldPaths] => [newPath]"} -func (b *BaseApi) MoveFile(c *gin.Context) { - var req request.FileMove - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := fileService.MvFile(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags File -// @Summary Download file -// @Description 下载文件 -// @Accept json -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/download [get] -func (b *BaseApi) Download(c *gin.Context) { - filePath := c.Query("path") - file, err := os.Open(filePath) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - } - defer file.Close() - info, _ := file.Stat() - c.Header("Content-Length", strconv.FormatInt(info.Size(), 10)) - c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(info.Name())) - http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), file) -} - -// @Tags File -// @Summary Chunk Download file -// @Description 分片下载下载文件 -// @Accept json -// @Param request body request.FileDownload true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/chunkdownload [post] -// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"下载文件 [name]","formatEN":"Download file [name]"} -func (b *BaseApi) DownloadChunkFiles(c *gin.Context) { - var req request.FileChunkDownload - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - fileOp := files.NewFileOp() - if !fileOp.Stat(req.Path) { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrPathNotFound, nil) - return - } - filePath := req.Path - fstFile, err := fileOp.OpenFile(filePath) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - info, err := fstFile.Stat() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - if info.IsDir() { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrFileDownloadDir, err) - return - } - - c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", req.Name)) - c.Writer.Header().Set("Content-Type", "application/octet-stream") - c.Writer.Header().Set("Content-Length", strconv.FormatInt(info.Size(), 10)) - c.Writer.Header().Set("Accept-Ranges", "bytes") - - if c.Request.Header.Get("Range") != "" { - rangeHeader := c.Request.Header.Get("Range") - rangeArr := strings.Split(rangeHeader, "=")[1] - rangeParts := strings.Split(rangeArr, "-") - - startPos, _ := strconv.ParseInt(rangeParts[0], 10, 64) - - var endPos int64 - if rangeParts[1] == "" { - endPos = info.Size() - 1 - } else { - endPos, _ = strconv.ParseInt(rangeParts[1], 10, 64) - } - - c.Writer.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", startPos, endPos, info.Size())) - c.Writer.WriteHeader(http.StatusPartialContent) - - buffer := make([]byte, 1024*1024) - file, err := os.Open(filePath) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - defer file.Close() - - _, _ = file.Seek(startPos, 0) - reader := io.LimitReader(file, endPos-startPos+1) - _, err = io.CopyBuffer(c.Writer, reader, buffer) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - } else { - c.File(filePath) - } -} - -// @Tags File -// @Summary Load file size -// @Description 获取文件夹大小 -// @Accept json -// @Param request body request.DirSizeReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/size [post] -// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"获取文件夹大小 [path]","formatEN":"Load file size [path]"} -func (b *BaseApi) Size(c *gin.Context) { - var req request.DirSizeReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - res, err := fileService.DirSize(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, res) -} - -func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int, overwrite bool) error { - defer func() { - _ = os.RemoveAll(fileDir) - }() - - op := files.NewFileOp() - dstDir = strings.TrimSpace(dstDir) - mode, _ := files.GetParentMode(dstDir) - if mode == 0 { - mode = 0755 - } - if _, err := os.Stat(dstDir); err != nil && os.IsNotExist(err) { - if err = op.CreateDir(dstDir, mode); err != nil { - return err - } - } - dstFileName := filepath.Join(dstDir, fileName) - dstInfo, statErr := os.Stat(dstFileName) - if statErr == nil { - mode = dstInfo.Mode() - } else { - mode = 0644 - } - if overwrite { - _ = os.Remove(dstFileName) - } - targetFile, err := os.OpenFile(dstFileName, os.O_RDWR|os.O_CREATE, mode) - if err != nil { - return err - } - defer targetFile.Close() - for i := 0; i < chunkCount; i++ { - chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", fileName, i)) - chunkData, err := os.ReadFile(chunkPath) - if err != nil { - return err - } - _, err = targetFile.Write(chunkData) - if err != nil { - return err - } - _ = os.Remove(chunkPath) - } - - return nil -} - -// @Tags File -// @Summary ChunkUpload file -// @Description 分片上传文件 -// @Param file formData file true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/chunkupload [post] -func (b *BaseApi) UploadChunkFiles(c *gin.Context) { - var err error - fileForm, err := c.FormFile("chunk") - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - uploadFile, err := fileForm.Open() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - defer uploadFile.Close() - chunkIndex, err := strconv.Atoi(c.PostForm("chunkIndex")) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - chunkCount, err := strconv.Atoi(c.PostForm("chunkCount")) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - fileOp := files.NewFileOp() - tmpDir := path.Join(global.CONF.System.TmpDir, "upload") - if !fileOp.Stat(tmpDir) { - if err := fileOp.CreateDir(tmpDir, 0755); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - } - filename := c.PostForm("filename") - fileDir := filepath.Join(tmpDir, filename) - if chunkIndex == 0 { - if fileOp.Stat(fileDir) { - _ = fileOp.DeleteDir(fileDir) - } - _ = os.MkdirAll(fileDir, 0755) - } - filePath := filepath.Join(fileDir, filename) - - defer func() { - if err != nil { - _ = os.Remove(fileDir) - } - }() - var ( - emptyFile *os.File - chunkData []byte - ) - - emptyFile, err = os.Create(filePath) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - defer emptyFile.Close() - - chunkData, err = io.ReadAll(uploadFile) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, buserr.WithMap(constant.ErrFileUpload, map[string]interface{}{"name": filename, "detail": err.Error()}, err)) - return - } - - chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex)) - err = os.WriteFile(chunkPath, chunkData, 0644) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, buserr.WithMap(constant.ErrFileUpload, map[string]interface{}{"name": filename, "detail": err.Error()}, err)) - return - } - - if chunkIndex+1 == chunkCount { - overwrite := true - if ow := c.PostForm("overwrite"); ow != "" { - overwrite, _ = strconv.ParseBool(ow) - } - err = mergeChunks(filename, fileDir, c.PostForm("path"), chunkCount, overwrite) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, buserr.WithMap(constant.ErrFileUpload, map[string]interface{}{"name": filename, "detail": err.Error()}, err)) - return - } - helper.SuccessWithData(c, true) - } else { - return - } -} - -var wsUpgrade = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { - return true - }, -} - -func (b *BaseApi) Ws(c *gin.Context) { - ws, err := wsUpgrade.Upgrade(c.Writer, c.Request, nil) - if err != nil { - return - } - wsClient := websocket2.NewWsClient("fileClient", ws) - go wsClient.Read() - go wsClient.Write() -} - -func (b *BaseApi) Keys(c *gin.Context) { - res := &response.FileProcessKeys{} - keys, err := global.CACHE.PrefixScanKey("file-wget-") - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - res.Keys = keys - helper.SuccessWithData(c, res) -} - -// @Tags File -// @Summary Read file by Line -// @Description 按行读取日志文件 -// @Param request body request.FileReadByLineReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/read [post] -func (b *BaseApi) ReadFileByLine(c *gin.Context) { - var req request.FileReadByLineReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - res, err := fileService.ReadLogByLine(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, res) -} - -// @Tags File -// @Summary Batch change file mode and owner -// @Description 批量修改文件权限和用户/组 -// @Accept json -// @Param request body request.FileRoleReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/batch/role [post] -// @x-panel-log {"bodyKeys":["paths","mode","user","group"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"批量修改文件权限和用户/组 [paths] => [mode]/[user]/[group]","formatEN":"Batch change file mode and owner [paths] => [mode]/[user]/[group]"} -func (b *BaseApi) BatchChangeModeAndOwner(c *gin.Context) { - var req request.FileRoleReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := fileService.BatchChangeModeAndOwner(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - } - helper.SuccessWithOutData(c) -} diff --git a/backend/app/api/v1/firewall.go b/backend/app/api/v1/firewall.go deleted file mode 100644 index b509e7064..000000000 --- a/backend/app/api/v1/firewall.go +++ /dev/null @@ -1,224 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Firewall -// @Summary Load firewall base info -// @Description 获取防火墙基础信息 -// @Success 200 {object} dto.FirewallBaseInfo -// @Security ApiKeyAuth -// @Router /hosts/firewall/base [get] -func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) { - data, err := firewallService.LoadBaseInfo() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, data) -} - -// @Tags Firewall -// @Summary Page firewall rules -// @Description 获取防火墙规则列表分页 -// @Accept json -// @Param request body dto.RuleSearch true "request" -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /hosts/firewall/search [post] -func (b *BaseApi) SearchFirewallRule(c *gin.Context) { - var req dto.RuleSearch - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - total, list, err := firewallService.SearchWithPage(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - -// @Tags Firewall -// @Summary Page firewall status -// @Description 修改防火墙状态 -// @Accept json -// @Param request body dto.FirewallOperation true "request" -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /hosts/firewall/operate [post] -// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] 防火墙","formatEN":"[operation] firewall"} -func (b *BaseApi) OperateFirewall(c *gin.Context) { - var req dto.FirewallOperation - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := firewallService.OperateFirewall(req.Operation); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -// @Tags Firewall -// @Summary Create group -// @Description 创建防火墙端口规则 -// @Accept json -// @Param request body dto.PortRuleOperate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /hosts/firewall/port [post] -// @x-panel-log {"bodyKeys":["port","strategy"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"添加端口规则 [strategy] [port]","formatEN":"create port rules [strategy][port]"} -func (b *BaseApi) OperatePortRule(c *gin.Context) { - var req dto.PortRuleOperate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := firewallService.OperatePortRule(req, true); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// OperateForwardRule -// @Tags Firewall -// @Summary Create group -// @Description 更新防火墙端口转发规则 -// @Accept json -// @Param request body dto.ForwardRuleOperate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /hosts/firewall/forward [post] -// @x-panel-log {"bodyKeys":["source_port"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新端口转发规则 [source_port]","formatEN":"update port forward rules [source_port]"} -func (b *BaseApi) OperateForwardRule(c *gin.Context) { - var req dto.ForwardRuleOperate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := firewallService.OperateForwardRule(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Firewall -// @Summary Create group -// @Description 创建防火墙 IP 规则 -// @Accept json -// @Param request body dto.AddrRuleOperate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /hosts/firewall/ip [post] -// @x-panel-log {"bodyKeys":["strategy","address"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"添加 ip 规则 [strategy] [address]","formatEN":"create address rules [strategy][address]"} -func (b *BaseApi) OperateIPRule(c *gin.Context) { - var req dto.AddrRuleOperate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := firewallService.OperateAddressRule(req, true); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Firewall -// @Summary Create group -// @Description 批量删除防火墙规则 -// @Accept json -// @Param request body dto.BatchRuleOperate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /hosts/firewall/batch [post] -func (b *BaseApi) BatchOperateRule(c *gin.Context) { - var req dto.BatchRuleOperate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := firewallService.BatchOperateRule(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Firewall -// @Summary Update rule description -// @Description 更新防火墙描述 -// @Accept json -// @Param request body dto.UpdateFirewallDescription true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /hosts/firewall/update/description [post] -func (b *BaseApi) UpdateFirewallDescription(c *gin.Context) { - var req dto.UpdateFirewallDescription - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := firewallService.UpdateDescription(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Firewall -// @Summary Create group -// @Description 更新端口防火墙规则 -// @Accept json -// @Param request body dto.PortRuleUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /hosts/firewall/update/port [post] -func (b *BaseApi) UpdatePortRule(c *gin.Context) { - var req dto.PortRuleUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := firewallService.UpdatePortRule(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Firewall -// @Summary Create group -// @Description 更新 ip 防火墙规则 -// @Accept json -// @Param request body dto.AddrRuleUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /hosts/firewall/update/addr [post] -func (b *BaseApi) UpdateAddrRule(c *gin.Context) { - var req dto.AddrRuleUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := firewallService.UpdateAddrRule(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} diff --git a/backend/app/api/v1/ftp.go b/backend/app/api/v1/ftp.go deleted file mode 100644 index 4241013af..000000000 --- a/backend/app/api/v1/ftp.go +++ /dev/null @@ -1,199 +0,0 @@ -package v1 - -import ( - "encoding/base64" - - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags FTP -// @Summary Load FTP base info -// @Description 获取 FTP 基础信息 -// @Success 200 {object} dto.FtpBaseInfo -// @Security ApiKeyAuth -// @Router /toolbox/ftp/base [get] -func (b *BaseApi) LoadFtpBaseInfo(c *gin.Context) { - data, err := ftpService.LoadBaseInfo() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, data) -} - -// @Tags FTP -// @Summary Load FTP operation log -// @Description 获取 FTP 操作日志 -// @Accept json -// @Param request body dto.FtpLogSearch true "request" -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /toolbox/ftp/log/search [post] -func (b *BaseApi) LoadFtpLogInfo(c *gin.Context) { - var req dto.FtpLogSearch - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - total, list, err := ftpService.LoadLog(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - -// @Tags FTP -// @Summary Operate FTP -// @Description 修改 FTP 状态 -// @Accept json -// @Param request body dto.Operate true "request" -// @Security ApiKeyAuth -// @Router /toolbox/ftp/operate [post] -// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] FTP","formatEN":"[operation] FTP"} -func (b *BaseApi) OperateFtp(c *gin.Context) { - var req dto.Operate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := ftpService.Operate(req.Operation); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -// @Tags FTP -// @Summary Page FTP user -// @Description 获取 FTP 账户列表分页 -// @Accept json -// @Param request body dto.SearchWithPage true "request" -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /toolbox/ftp/search [post] -func (b *BaseApi) SearchFtp(c *gin.Context) { - var req dto.SearchWithPage - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - total, list, err := ftpService.SearchWithPage(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - -// @Tags FTP -// @Summary Create FTP user -// @Description 创建 FTP 账户 -// @Accept json -// @Param request body dto.FtpCreate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /toolbox/ftp [post] -// @x-panel-log {"bodyKeys":["user", "path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 FTP 账户 [user][path]","formatEN":"create FTP [user][path]"} -func (b *BaseApi) CreateFtp(c *gin.Context) { - var req dto.FtpCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if len(req.Password) != 0 { - pass, err := base64.StdEncoding.DecodeString(req.Password) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - req.Password = string(pass) - } - if _, err := ftpService.Create(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags FTP -// @Summary Delete FTP user -// @Description 删除 FTP 账户 -// @Accept json -// @Param request body dto.BatchDeleteReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /toolbox/ftp/del [post] -// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"ftps","output_column":"user","output_value":"users"}],"formatZH":"删除 FTP 账户 [users]","formatEN":"delete FTP users [users]"} -func (b *BaseApi) DeleteFtp(c *gin.Context) { - var req dto.BatchDeleteReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := ftpService.Delete(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags FTP -// @Summary Sync FTP user -// @Description 同步 FTP 账户 -// @Accept json -// @Param request body dto.BatchDeleteReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /toolbox/ftp/sync [post] -// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 FTP 账户","formatEN":"sync FTP users"} -func (b *BaseApi) SyncFtp(c *gin.Context) { - if err := ftpService.Sync(); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags FTP -// @Summary Update FTP user -// @Description 修改 FTP 账户 -// @Accept json -// @Param request body dto.FtpUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /toolbox/ftp/update [post] -// @x-panel-log {"bodyKeys":["user", "path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 FTP 账户 [user][path]","formatEN":"update FTP [user][path]"} -func (b *BaseApi) UpdateFtp(c *gin.Context) { - var req dto.FtpUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if len(req.Password) != 0 { - pass, err := base64.StdEncoding.DecodeString(req.Password) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - req.Password = string(pass) - } - if err := ftpService.Update(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} diff --git a/backend/app/api/v1/group.go b/backend/app/api/v1/group.go deleted file mode 100644 index 95ce11deb..000000000 --- a/backend/app/api/v1/group.go +++ /dev/null @@ -1,97 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags System Group -// @Summary Create group -// @Description 创建系统组 -// @Accept json -// @Param request body dto.GroupCreate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /groups [post] -// @x-panel-log {"bodyKeys":["name","type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建组 [name][type]","formatEN":"create group [name][type]"} -func (b *BaseApi) CreateGroup(c *gin.Context) { - var req dto.GroupCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := groupService.Create(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags System Group -// @Summary Delete group -// @Description 删除系统组 -// @Accept json -// @Param request body dto.OperateByID true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /groups/del [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"groups","output_column":"name","output_value":"name"},{"input_column":"id","input_value":"id","isList":false,"db":"groups","output_column":"type","output_value":"type"}],"formatZH":"删除组 [type][name]","formatEN":"delete group [type][name]"} -func (b *BaseApi) DeleteGroup(c *gin.Context) { - var req dto.OperateByID - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := groupService.Delete(req.ID); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags System Group -// @Summary Update group -// @Description 更新系统组 -// @Accept json -// @Param request body dto.GroupUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /groups/update [post] -// @x-panel-log {"bodyKeys":["name","type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新组 [name][type]","formatEN":"update group [name][type]"} -func (b *BaseApi) UpdateGroup(c *gin.Context) { - var req dto.GroupUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := groupService.Update(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags System Group -// @Summary List groups -// @Description 查询系统组 -// @Accept json -// @Param request body dto.GroupSearch true "request" -// @Success 200 {array} dto.GroupInfo -// @Security ApiKeyAuth -// @Router /groups/search [post] -func (b *BaseApi) ListGroup(c *gin.Context) { - var req dto.GroupSearch - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - list, err := groupService.List(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, list) -} diff --git a/backend/app/api/v1/helper/helper.go b/backend/app/api/v1/helper/helper.go deleted file mode 100644 index aa4afa8fa..000000000 --- a/backend/app/api/v1/helper/helper.go +++ /dev/null @@ -1,140 +0,0 @@ -package helper - -import ( - "context" - "fmt" - "net/http" - "strconv" - - "github.com/1Panel-dev/1Panel/backend/global" - "gorm.io/gorm" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/i18n" - "github.com/gin-gonic/gin" - "github.com/pkg/errors" -) - -func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) { - res := dto.Response{ - Code: code, - Message: "", - } - if msgKey == constant.ErrTypeInternalServer { - switch { - case errors.Is(err, constant.ErrRecordExist): - res.Message = i18n.GetMsgWithMap("ErrRecordExist", nil) - case errors.Is(constant.ErrRecordNotFound, err): - res.Message = i18n.GetMsgWithMap("ErrRecordNotFound", nil) - case errors.Is(constant.ErrInvalidParams, err): - res.Message = i18n.GetMsgWithMap("ErrInvalidParams", nil) - case errors.Is(constant.ErrStructTransform, err): - res.Message = i18n.GetMsgWithMap("ErrStructTransform", map[string]interface{}{"detail": err}) - case errors.Is(constant.ErrCaptchaCode, err): - res.Code = constant.CodeAuth - res.Message = "ErrCaptchaCode" - case errors.Is(constant.ErrAuth, err): - res.Code = constant.CodeAuth - res.Message = "ErrAuth" - case errors.Is(constant.ErrInitialPassword, err): - res.Message = i18n.GetMsgWithMap("ErrInitialPassword", map[string]interface{}{"detail": err}) - case errors.As(err, &buserr.BusinessError{}): - res.Message = err.Error() - default: - res.Message = i18n.GetMsgWithMap(msgKey, map[string]interface{}{"detail": err}) - } - } else { - res.Message = i18n.GetMsgWithMap(msgKey, map[string]interface{}{"detail": err}) - } - ctx.JSON(http.StatusOK, res) - ctx.Abort() -} - -func SuccessWithData(ctx *gin.Context, data interface{}) { - if data == nil { - data = gin.H{} - } - res := dto.Response{ - Code: constant.CodeSuccess, - Data: data, - } - ctx.JSON(http.StatusOK, res) - ctx.Abort() -} - -func SuccessWithOutData(ctx *gin.Context) { - res := dto.Response{ - Code: constant.CodeSuccess, - Message: "success", - } - ctx.JSON(http.StatusOK, res) - ctx.Abort() -} - -func SuccessWithMsg(ctx *gin.Context, msg string) { - res := dto.Response{ - Code: constant.CodeSuccess, - Message: msg, - } - ctx.JSON(http.StatusOK, res) - ctx.Abort() -} - -func GetParamID(c *gin.Context) (uint, error) { - idParam, ok := c.Params.Get("id") - if !ok { - return 0, errors.New("error id in path") - } - intNum, _ := strconv.Atoi(idParam) - return uint(intNum), nil -} - -func GetIntParamByKey(c *gin.Context, key string) (uint, error) { - idParam, ok := c.Params.Get(key) - if !ok { - return 0, fmt.Errorf("error %s in path", key) - } - intNum, _ := strconv.Atoi(idParam) - return uint(intNum), nil -} - -func GetStrParamByKey(c *gin.Context, key string) (string, error) { - idParam, ok := c.Params.Get(key) - if !ok { - return "", fmt.Errorf("error %s in path", key) - } - return idParam, nil -} - -func GetTxAndContext() (tx *gorm.DB, ctx context.Context) { - tx = global.DB.Begin() - ctx = context.WithValue(context.Background(), constant.DB, tx) - return -} - -func CheckBindAndValidate(req interface{}, c *gin.Context) error { - if err := c.ShouldBindJSON(req); err != nil { - ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return err - } - if err := global.VALID.Struct(req); err != nil { - ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return err - } - return nil -} - -func CheckBind(req interface{}, c *gin.Context) error { - if err := c.ShouldBindJSON(&req); err != nil { - ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return err - } - return nil -} - -func ErrResponse(ctx *gin.Context, code int) { - ctx.JSON(code, nil) - ctx.Abort() -} diff --git a/backend/app/api/v1/host.go b/backend/app/api/v1/host.go deleted file mode 100644 index 1789b028f..000000000 --- a/backend/app/api/v1/host.go +++ /dev/null @@ -1,230 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/encrypt" - "github.com/gin-gonic/gin" -) - -// @Tags Host -// @Summary Create host -// @Description 创建主机 -// @Accept json -// @Param request body dto.HostOperate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /hosts [post] -// @x-panel-log {"bodyKeys":["name","addr"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建主机 [name][addr]","formatEN":"create host [name][addr]"} -func (b *BaseApi) CreateHost(c *gin.Context) { - var req dto.HostOperate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - host, err := hostService.Create(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, host) -} - -// @Tags Host -// @Summary Test host conn by info -// @Description 测试主机连接 -// @Accept json -// @Param request body dto.HostConnTest true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /hosts/test/byinfo [post] -func (b *BaseApi) TestByInfo(c *gin.Context) { - var req dto.HostConnTest - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - connStatus := hostService.TestByInfo(req) - helper.SuccessWithData(c, connStatus) -} - -// @Tags Host -// @Summary Test host conn by host id -// @Description 测试主机连接 -// @Accept json -// @Param id path integer true "request" -// @Success 200 {boolean} connStatus -// @Security ApiKeyAuth -// @Router /hosts/test/byid/:id [post] -func (b *BaseApi) TestByID(c *gin.Context) { - id, err := helper.GetParamID(c) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - - connStatus := hostService.TestLocalConn(id) - helper.SuccessWithData(c, connStatus) -} - -// @Tags Host -// @Summary Load host tree -// @Description 加载主机树 -// @Accept json -// @Param request body dto.SearchForTree true "request" -// @Success 200 {array} dto.HostTree -// @Security ApiKeyAuth -// @Router /hosts/tree [post] -func (b *BaseApi) HostTree(c *gin.Context) { - var req dto.SearchForTree - if err := helper.CheckBind(&req, c); err != nil { - return - } - - data, err := hostService.SearchForTree(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, data) -} - -// @Tags Host -// @Summary Page host -// @Description 获取主机列表分页 -// @Accept json -// @Param request body dto.SearchHostWithPage true "request" -// @Success 200 {array} dto.HostTree -// @Security ApiKeyAuth -// @Router /hosts/search [post] -func (b *BaseApi) SearchHost(c *gin.Context) { - var req dto.SearchHostWithPage - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - total, list, err := hostService.SearchWithPage(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - -// @Tags Host -// @Summary Delete host -// @Description 删除主机 -// @Accept json -// @Param request body dto.BatchDeleteReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /hosts/del [post] -// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"hosts","output_column":"addr","output_value":"addrs"}],"formatZH":"删除主机 [addrs]","formatEN":"delete host [addrs]"} -func (b *BaseApi) DeleteHost(c *gin.Context) { - var req dto.BatchDeleteReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := hostService.Delete(req.Ids); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Host -// @Summary Update host -// @Description 更新主机 -// @Accept json -// @Param request body dto.HostOperate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /hosts/update [post] -// @x-panel-log {"bodyKeys":["name","addr"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新主机信息 [name][addr]","formatEN":"update host [name][addr]"} -func (b *BaseApi) UpdateHost(c *gin.Context) { - var req dto.HostOperate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - var err error - if len(req.Password) != 0 && req.AuthMode == "password" { - req.Password, err = hostService.EncryptHost(req.Password) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - req.PrivateKey = "" - req.PassPhrase = "" - } - if len(req.PrivateKey) != 0 && req.AuthMode == "key" { - req.PrivateKey, err = hostService.EncryptHost(req.PrivateKey) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - if len(req.PassPhrase) != 0 { - req.PassPhrase, err = encrypt.StringEncrypt(req.PassPhrase) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - } - req.Password = "" - } - - upMap := make(map[string]interface{}) - upMap["name"] = req.Name - upMap["group_id"] = req.GroupID - upMap["addr"] = req.Addr - upMap["port"] = req.Port - upMap["user"] = req.User - upMap["auth_mode"] = req.AuthMode - upMap["remember_password"] = req.RememberPassword - if req.AuthMode == "password" { - upMap["password"] = req.Password - upMap["private_key"] = "" - upMap["pass_phrase"] = "" - } else { - upMap["password"] = "" - upMap["private_key"] = req.PrivateKey - upMap["pass_phrase"] = req.PassPhrase - } - upMap["description"] = req.Description - if err := hostService.Update(req.ID, upMap); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Host -// @Summary Update host group -// @Description 切换分组 -// @Accept json -// @Param request body dto.ChangeHostGroup true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /hosts/update/group [post] -// @x-panel-log {"bodyKeys":["id","group"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"hosts","output_column":"addr","output_value":"addr"}],"formatZH":"切换主机[addr]分组 => [group]","formatEN":"change host [addr] group => [group]"} -func (b *BaseApi) UpdateHostGroup(c *gin.Context) { - var req dto.ChangeHostGroup - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - upMap := make(map[string]interface{}) - upMap["group_id"] = req.GroupID - if err := hostService.Update(req.ID, upMap); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} diff --git a/backend/app/api/v1/host_tool.go b/backend/app/api/v1/host_tool.go deleted file mode 100644 index c55fb9bd8..000000000 --- a/backend/app/api/v1/host_tool.go +++ /dev/null @@ -1,180 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Host tool -// @Summary Get tool -// @Description 获取主机工具状态 -// @Accept json -// @Param request body request.HostToolReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /host/tool [post] -func (b *BaseApi) GetToolStatus(c *gin.Context) { - var req request.HostToolReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - config, err := hostToolService.GetToolStatus(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, config) -} - -// @Tags Host tool -// @Summary Create Host tool Config -// @Description 创建主机工具配置 -// @Accept json -// @Param request body request.HostToolCreate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /host/tool/create [post] -// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 [type] 配置","formatEN":"create [type] config"} -func (b *BaseApi) InitToolConfig(c *gin.Context) { - var req request.HostToolCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := hostToolService.CreateToolConfig(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags Host tool -// @Summary Operate tool -// @Description 操作主机工具 -// @Accept json -// @Param request body request.HostToolReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /host/tool/operate [post] -// @x-panel-log {"bodyKeys":["operate","type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] [type] ","formatEN":"[operate] [type]"} -func (b *BaseApi) OperateTool(c *gin.Context) { - var req request.HostToolReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - err := hostToolService.OperateTool(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags Host tool -// @Summary Get tool config -// @Description 操作主机工具配置文件 -// @Accept json -// @Param request body request.HostToolConfig true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /host/tool/config [post] -// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 主机工具配置文件 ","formatEN":"[operate] tool config"} -func (b *BaseApi) OperateToolConfig(c *gin.Context) { - var req request.HostToolConfig - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - config, err := hostToolService.OperateToolConfig(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, config) -} - -// @Tags Host tool -// @Summary Get tool -// @Description 获取主机工具日志 -// @Accept json -// @Param request body request.HostToolLogReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /host/tool/log [post] -func (b *BaseApi) GetToolLog(c *gin.Context) { - var req request.HostToolLogReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - logContent, err := hostToolService.GetToolLog(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, logContent) -} - -// @Tags Host tool -// @Summary Create Supervisor process -// @Description 操作守护进程 -// @Accept json -// @Param request body request.SupervisorProcessConfig true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /host/tool/supervisor/process [post] -// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 守护进程 ","formatEN":"[operate] process"} -func (b *BaseApi) OperateProcess(c *gin.Context) { - var req request.SupervisorProcessConfig - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - err := hostToolService.OperateSupervisorProcess(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags Host tool -// @Summary Get Supervisor process config -// @Description 获取 Supervisor 进程配置 -// @Accept json -// @Success 200 -// @Security ApiKeyAuth -// @Router /host/tool/supervisor/process [get] -func (b *BaseApi) GetProcess(c *gin.Context) { - configs, err := hostToolService.GetSupervisorProcessConfig() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, configs) -} - -// @Tags Host tool -// @Summary Get Supervisor process config -// @Description 操作 Supervisor 进程文件 -// @Accept json -// @Param request body request.SupervisorProcessFileReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /host/tool/supervisor/process/file [post] -// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] Supervisor 进程文件 ","formatEN":"[operate] Supervisor Process Config file"} -func (b *BaseApi) GetProcessFile(c *gin.Context) { - var req request.SupervisorProcessFileReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - content, err := hostToolService.OperateSupervisorProcessFile(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, content) -} diff --git a/backend/app/api/v1/image.go b/backend/app/api/v1/image.go deleted file mode 100644 index 6f3743eb3..000000000 --- a/backend/app/api/v1/image.go +++ /dev/null @@ -1,231 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Container Image -// @Summary Page images -// @Description 获取镜像列表分页 -// @Accept json -// @Param request body dto.SearchWithPage true "request" -// @Produce json -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /containers/image/search [post] -func (b *BaseApi) SearchImage(c *gin.Context) { - var req dto.SearchWithPage - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - total, list, err := imageService.Page(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - -// @Tags Container Image -// @Summary List all images -// @Description 获取所有镜像列表 -// @Produce json -// @Success 200 {array} dto.ImageInfo -// @Security ApiKeyAuth -// @Router /containers/image/all [get] -func (b *BaseApi) ListAllImage(c *gin.Context) { - list, err := imageService.ListAll() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, list) -} - -// @Tags Container Image -// @Summary load images options -// @Description 获取镜像名称列表 -// @Produce json -// @Success 200 {array} dto.Options -// @Security ApiKeyAuth -// @Router /containers/image [get] -func (b *BaseApi) ListImage(c *gin.Context) { - list, err := imageService.List() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, list) -} - -// @Tags Container Image -// @Summary Build image -// @Description 构建镜像 -// @Accept json -// @Param request body dto.ImageBuild true "request" -// @Success 200 {string} log -// @Security ApiKeyAuth -// @Router /containers/image/build [post] -// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"构建镜像 [name]","formatEN":"build image [name]"} -func (b *BaseApi) ImageBuild(c *gin.Context) { - var req dto.ImageBuild - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - log, err := imageService.ImageBuild(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, log) -} - -// @Tags Container Image -// @Summary Pull image -// @Description 拉取镜像 -// @Accept json -// @Param request body dto.ImagePull true "request" -// @Success 200 {string} log -// @Security ApiKeyAuth -// @Router /containers/image/pull [post] -// @x-panel-log {"bodyKeys":["repoID","imageName"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"repoID","isList":false,"db":"image_repos","output_column":"name","output_value":"reponame"}],"formatZH":"镜像拉取 [reponame][imageName]","formatEN":"image pull [reponame][imageName]"} -func (b *BaseApi) ImagePull(c *gin.Context) { - var req dto.ImagePull - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - logPath, err := imageService.ImagePull(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, logPath) -} - -// @Tags Container Image -// @Summary Push image -// @Description 推送镜像 -// @Accept json -// @Param request body dto.ImagePush true "request" -// @Success 200 {string} log -// @Security ApiKeyAuth -// @Router /containers/image/push [post] -// @x-panel-log {"bodyKeys":["repoID","tagName","name"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"repoID","isList":false,"db":"image_repos","output_column":"name","output_value":"reponame"}],"formatZH":"[tagName] 推送到 [reponame][name]","formatEN":"push [tagName] to [reponame][name]"} -func (b *BaseApi) ImagePush(c *gin.Context) { - var req dto.ImagePush - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - logPath, err := imageService.ImagePush(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, logPath) -} - -// @Tags Container Image -// @Summary Delete image -// @Description 删除镜像 -// @Accept json -// @Param request body dto.BatchDelete true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/image/remove [post] -// @x-panel-log {"bodyKeys":["names"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"移除镜像 [names]","formatEN":"remove image [names]"} -func (b *BaseApi) ImageRemove(c *gin.Context) { - var req dto.BatchDelete - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := imageService.ImageRemove(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -// @Tags Container Image -// @Summary Save image -// @Description 导出镜像 -// @Accept json -// @Param request body dto.ImageSave true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/image/save [post] -// @x-panel-log {"bodyKeys":["tagName","path","name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"保留 [tagName] 为 [path]/[name]","formatEN":"save [tagName] as [path]/[name]"} -func (b *BaseApi) ImageSave(c *gin.Context) { - var req dto.ImageSave - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := imageService.ImageSave(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -// @Tags Container Image -// @Summary Tag image -// @Description Tag 镜像 -// @Accept json -// @Param request body dto.ImageTag true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/image/tag [post] -// @x-panel-log {"bodyKeys":["repoID","targetName"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"repoID","isList":false,"db":"image_repos","output_column":"name","output_value":"reponame"}],"formatZH":"tag 镜像 [reponame][targetName]","formatEN":"tag image [reponame][targetName]"} -func (b *BaseApi) ImageTag(c *gin.Context) { - var req dto.ImageTag - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := imageService.ImageTag(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -// @Tags Container Image -// @Summary Load image -// @Description 导入镜像 -// @Accept json -// @Param request body dto.ImageLoad true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/image/load [post] -// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"从 [path] 加载镜像","formatEN":"load image from [path]"} -func (b *BaseApi) ImageLoad(c *gin.Context) { - var req dto.ImageLoad - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := imageService.ImageLoad(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} diff --git a/backend/app/api/v1/image_repo.go b/backend/app/api/v1/image_repo.go deleted file mode 100644 index b6e1c5c42..000000000 --- a/backend/app/api/v1/image_repo.go +++ /dev/null @@ -1,143 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Container Image-repo -// @Summary Page image repos -// @Description 获取镜像仓库列表分页 -// @Accept json -// @Param request body dto.SearchWithPage true "request" -// @Produce json -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /containers/repo/search [post] -func (b *BaseApi) SearchRepo(c *gin.Context) { - var req dto.SearchWithPage - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - total, list, err := imageRepoService.Page(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - -// @Tags Container Image-repo -// @Summary List image repos -// @Description 获取镜像仓库列表 -// @Produce json -// @Success 200 {array} dto.ImageRepoOption -// @Security ApiKeyAuth -// @Router /containers/repo [get] -func (b *BaseApi) ListRepo(c *gin.Context) { - list, err := imageRepoService.List() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, list) -} - -// @Tags Container Image-repo -// @Summary Load repo status -// @Description 获取 docker 仓库状态 -// @Accept json -// @Param request body dto.OperateByID true "request" -// @Produce json -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/repo/status [get] -func (b *BaseApi) CheckRepoStatus(c *gin.Context) { - var req dto.OperateByID - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := imageRepoService.Login(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Container Image-repo -// @Summary Create image repo -// @Description 创建镜像仓库 -// @Accept json -// @Param request body dto.ImageRepoDelete true "request" -// @Produce json -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/repo [post] -// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建镜像仓库 [name]","formatEN":"create image repo [name]"} -func (b *BaseApi) CreateRepo(c *gin.Context) { - var req dto.ImageRepoCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := imageRepoService.Create(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Container Image-repo -// @Summary Delete image repo -// @Description 删除镜像仓库 -// @Accept json -// @Param request body dto.ImageRepoDelete true "request" -// @Produce json -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/repo/del [post] -// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"image_repos","output_column":"name","output_value":"names"}],"formatZH":"删除镜像仓库 [names]","formatEN":"delete image repo [names]"} -func (b *BaseApi) DeleteRepo(c *gin.Context) { - var req dto.ImageRepoDelete - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := imageRepoService.BatchDelete(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Container Image-repo -// @Summary Update image repo -// @Description 更新镜像仓库 -// @Accept json -// @Param request body dto.ImageRepoUpdate true "request" -// @Produce json -// @Success 200 -// @Security ApiKeyAuth -// @Router /containers/repo/update [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"image_repos","output_column":"name","output_value":"name"}],"formatZH":"更新镜像仓库 [name]","formatEN":"update image repo information [name]"} -func (b *BaseApi) UpdateRepo(c *gin.Context) { - var req dto.ImageRepoUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := imageRepoService.Update(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} diff --git a/backend/app/api/v1/logs.go b/backend/app/api/v1/logs.go deleted file mode 100644 index 311b5159b..000000000 --- a/backend/app/api/v1/logs.go +++ /dev/null @@ -1,45 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Logs -// @Summary Load system log files -// @Description 获取系统日志文件列表 -// @Success 200 -// @Security ApiKeyAuth -// @Router /logs/system/files [get] -func (b *BaseApi) GetSystemFiles(c *gin.Context) { - data, err := logService.ListSystemLogFile() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, data) -} - -// @Tags Logs -// @Summary Load system logs -// @Description 获取系统日志 -// @Success 200 -// @Security ApiKeyAuth -// @Router /logs/system [post] -func (b *BaseApi) GetSystemLogs(c *gin.Context) { - var req dto.OperationWithName - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - data, err := logService.LoadSystemLog(req.Name) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, data) -} diff --git a/backend/app/api/v1/monitor.go b/backend/app/api/v1/monitor.go deleted file mode 100644 index 0f7a6e767..000000000 --- a/backend/app/api/v1/monitor.go +++ /dev/null @@ -1,134 +0,0 @@ -package v1 - -import ( - "sort" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/gin-gonic/gin" - "github.com/shirou/gopsutil/v3/disk" - "github.com/shirou/gopsutil/v3/net" -) - -// @Tags Monitor -// @Summary Load monitor datas -// @Description 获取监控数据 -// @Param request body dto.MonitorSearch true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /hosts/monitor/search [post] -func (b *BaseApi) LoadMonitor(c *gin.Context) { - var req dto.MonitorSearch - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - loc, _ := time.LoadLocation(common.LoadTimeZone()) - req.StartTime = req.StartTime.In(loc) - req.EndTime = req.EndTime.In(loc) - - var backdatas []dto.MonitorData - if req.Param == "all" || req.Param == "cpu" || req.Param == "memory" || req.Param == "load" { - var bases []model.MonitorBase - if err := global.MonitorDB. - Where("created_at > ? AND created_at < ?", req.StartTime, req.EndTime). - Find(&bases).Error; err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - var itemData dto.MonitorData - itemData.Param = "base" - for _, base := range bases { - itemData.Date = append(itemData.Date, base.CreatedAt) - itemData.Value = append(itemData.Value, base) - } - backdatas = append(backdatas, itemData) - } - if req.Param == "all" || req.Param == "io" { - var bases []model.MonitorIO - if err := global.MonitorDB. - Where("created_at > ? AND created_at < ?", req.StartTime, req.EndTime). - Find(&bases).Error; err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - var itemData dto.MonitorData - itemData.Param = "io" - for _, base := range bases { - itemData.Date = append(itemData.Date, base.CreatedAt) - itemData.Value = append(itemData.Value, base) - } - backdatas = append(backdatas, itemData) - } - if req.Param == "all" || req.Param == "network" { - var bases []model.MonitorNetwork - if err := global.MonitorDB. - Where("name = ? AND created_at > ? AND created_at < ?", req.Info, req.StartTime, req.EndTime). - Find(&bases).Error; err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - var itemData dto.MonitorData - itemData.Param = "network" - for _, base := range bases { - itemData.Date = append(itemData.Date, base.CreatedAt) - itemData.Value = append(itemData.Value, base) - } - backdatas = append(backdatas, itemData) - } - helper.SuccessWithData(c, backdatas) -} - -// @Tags Monitor -// @Summary Clean monitor datas -// @Description 清空监控数据 -// @Success 200 -// @Security ApiKeyAuth -// @Router /hosts/monitor/clean [post] -// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空监控数据","formatEN":"clean monitor datas"} -func (b *BaseApi) CleanMonitor(c *gin.Context) { - if err := global.MonitorDB.Exec("DELETE FROM monitor_bases").Error; err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - if err := global.MonitorDB.Exec("DELETE FROM monitor_ios").Error; err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - if err := global.MonitorDB.Exec("DELETE FROM monitor_networks").Error; err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, nil) -} - -func (b *BaseApi) GetNetworkOptions(c *gin.Context) { - netStat, _ := net.IOCounters(true) - var options []string - options = append(options, "all") - for _, net := range netStat { - options = append(options, net.Name) - } - sort.Strings(options) - helper.SuccessWithData(c, options) -} - -func (b *BaseApi) GetIOOptions(c *gin.Context) { - diskStat, _ := disk.IOCounters() - var options []string - options = append(options, "all") - for _, net := range diskStat { - options = append(options, net.Name) - } - sort.Strings(options) - helper.SuccessWithData(c, options) -} diff --git a/backend/app/api/v1/nginx.go b/backend/app/api/v1/nginx.go deleted file mode 100644 index 8249f19dc..000000000 --- a/backend/app/api/v1/nginx.go +++ /dev/null @@ -1,118 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags OpenResty -// @Summary Load OpenResty conf -// @Description 获取 OpenResty 配置信息 -// @Success 200 {object} response.FileInfo -// @Security ApiKeyAuth -// @Router /openresty [get] -func (b *BaseApi) GetNginx(c *gin.Context) { - fileInfo, err := nginxService.GetNginxConfig() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, fileInfo) -} - -// @Tags OpenResty -// @Summary Load partial OpenResty conf -// @Description 获取部分 OpenResty 配置信息 -// @Accept json -// @Param request body request.NginxScopeReq true "request" -// @Success 200 {array} response.NginxParam -// @Security ApiKeyAuth -// @Router /openresty/scope [post] -func (b *BaseApi) GetNginxConfigByScope(c *gin.Context) { - var req request.NginxScopeReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - params, err := nginxService.GetConfigByScope(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, params) -} - -// @Tags OpenResty -// @Summary Update OpenResty conf -// @Description 更新 OpenResty 配置信息 -// @Accept json -// @Param request body request.NginxConfigUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /openresty/update [post] -// @x-panel-log {"bodyKeys":["websiteId"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteId","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"更新 nginx 配置 [domain]","formatEN":"Update nginx conf [domain]"} -func (b *BaseApi) UpdateNginxConfigByScope(c *gin.Context) { - var req request.NginxConfigUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := nginxService.UpdateConfigByScope(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags OpenResty -// @Summary Load OpenResty status info -// @Description 获取 OpenResty 状态信息 -// @Success 200 {object} response.NginxStatus -// @Security ApiKeyAuth -// @Router /openresty/status [get] -func (b *BaseApi) GetNginxStatus(c *gin.Context) { - res, err := nginxService.GetStatus() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, res) -} - -// @Tags OpenResty -// @Summary Update OpenResty conf by upload file -// @Description 上传更新 OpenResty 配置文件 -// @Accept json -// @Param request body request.NginxConfigFileUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /openresty/file [post] -// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 nginx 配置","formatEN":"Update nginx conf"} -func (b *BaseApi) UpdateNginxFile(c *gin.Context) { - var req request.NginxConfigFileUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := nginxService.UpdateConfigFile(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags OpenResty -// @Summary Clear OpenResty proxy cache -// @Description 清理 OpenResty 代理缓存 -// @Success 200 -// @Security ApiKeyAuth -// @Router /openresty/clear [post] -// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清理 Openresty 代理缓存","formatEN":"Clear nginx proxy cache"} -func (b *BaseApi) ClearNginxProxyCache(c *gin.Context) { - if err := nginxService.ClearProxyCache(); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} diff --git a/backend/app/api/v1/php_extensions.go b/backend/app/api/v1/php_extensions.go deleted file mode 100644 index 9af31f4f1..000000000 --- a/backend/app/api/v1/php_extensions.go +++ /dev/null @@ -1,103 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags PHP Extensions -// @Summary Page Extensions -// @Description Page Extensions -// @Accept json -// @Param request body request.PHPExtensionsSearch true "request" -// @Success 200 {array} response.PHPExtensionsDTO -// @Security ApiKeyAuth -// @Router /runtimes/php/extensions/search [post] -func (b *BaseApi) PagePHPExtensions(c *gin.Context) { - var req request.PHPExtensionsSearch - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if req.All { - list, err := phpExtensionsService.List() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, list) - } else { - total, list, err := phpExtensionsService.Page(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, dto.PageResult{ - Total: total, - Items: list, - }) - } - -} - -// @Tags PHP Extensions -// @Summary Create Extensions -// @Description Create Extensions -// @Accept json -// @Param request body request.PHPExtensionsCreate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /runtimes/php/extensions [post] -func (b *BaseApi) CreatePHPExtensions(c *gin.Context) { - var req request.PHPExtensionsCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := phpExtensionsService.Create(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags PHP Extensions -// @Summary Update Extensions -// @Description Update Extensions -// @Accept json -// @Param request body request.PHPExtensionsUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /runtimes/php/extensions/update [post] -func (b *BaseApi) UpdatePHPExtensions(c *gin.Context) { - var req request.PHPExtensionsUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := phpExtensionsService.Update(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags PHP Extensions -// @Summary Delete Extensions -// @Description Delete Extensions -// @Accept json -// @Param request body request.PHPExtensionsDelete true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /runtimes/php/extensions/del [post] -func (b *BaseApi) DeletePHPExtensions(c *gin.Context) { - var req request.PHPExtensionsDelete - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := phpExtensionsService.Delete(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} diff --git a/backend/app/api/v1/process.go b/backend/app/api/v1/process.go deleted file mode 100644 index 70666ac09..000000000 --- a/backend/app/api/v1/process.go +++ /dev/null @@ -1,39 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/constant" - websocket2 "github.com/1Panel-dev/1Panel/backend/utils/websocket" - "github.com/gin-gonic/gin" -) - -func (b *BaseApi) ProcessWs(c *gin.Context) { - ws, err := wsUpgrade.Upgrade(c.Writer, c.Request, nil) - if err != nil { - return - } - wsClient := websocket2.NewWsClient("processClient", ws) - go wsClient.Read() - go wsClient.Write() -} - -// @Tags Process -// @Summary Stop Process -// @Description 停止进程 -// @Param request body request.ProcessReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /process/stop [post] -// @x-panel-log {"bodyKeys":["PID"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"结束进程 [PID]","formatEN":"结束进程 [PID]"} -func (b *BaseApi) StopProcess(c *gin.Context) { - var req request.ProcessReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := processService.StopProcess(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - helper.SuccessWithOutData(c) -} diff --git a/backend/app/api/v1/recycle_bin.go b/backend/app/api/v1/recycle_bin.go deleted file mode 100644 index 0c7794e01..000000000 --- a/backend/app/api/v1/recycle_bin.go +++ /dev/null @@ -1,86 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags File -// @Summary List RecycleBin files -// @Description 获取回收站文件列表 -// @Accept json -// @Param request body dto.PageInfo true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/recycle/search [post] -func (b *BaseApi) SearchRecycleBinFile(c *gin.Context) { - var req dto.PageInfo - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - total, list, err := recycleBinService.Page(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - -// @Tags File -// @Summary Reduce RecycleBin files -// @Description 还原回收站文件 -// @Accept json -// @Param request body request.RecycleBinReduce true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/recycle/reduce [post] -// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"还原回收站文件 [name]","formatEN":"Reduce RecycleBin file [name]"} -func (b *BaseApi) ReduceRecycleBinFile(c *gin.Context) { - var req request.RecycleBinReduce - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := recycleBinService.Reduce(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags File -// @Summary Clear RecycleBin files -// @Description 清空回收站文件 -// @Accept json -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/recycle/clear [post] -// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空回收站","formatEN":"清空回收站"} -func (b *BaseApi) ClearRecycleBinFile(c *gin.Context) { - if err := recycleBinService.Clear(); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags File -// @Summary Get RecycleBin status -// @Description 获取回收站状态 -// @Accept json -// @Success 200 -// @Security ApiKeyAuth -// @Router /files/recycle/status [get] -func (b *BaseApi) GetRecycleStatus(c *gin.Context) { - settingInfo, err := settingService.GetSettingInfo() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, settingInfo.FileRecycleBin) -} diff --git a/backend/app/api/v1/runtime.go b/backend/app/api/v1/runtime.go deleted file mode 100644 index e4eef9e86..000000000 --- a/backend/app/api/v1/runtime.go +++ /dev/null @@ -1,235 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Runtime -// @Summary List runtimes -// @Description 获取运行环境列表 -// @Accept json -// @Param request body request.RuntimeSearch true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /runtimes/search [post] -func (b *BaseApi) SearchRuntimes(c *gin.Context) { - var req request.RuntimeSearch - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - total, items, err := runtimeService.Page(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, dto.PageResult{ - Total: total, - Items: items, - }) -} - -// @Tags Runtime -// @Summary Create runtime -// @Description 创建运行环境 -// @Accept json -// @Param request body request.RuntimeCreate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /runtimes [post] -// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建运行环境 [name]","formatEN":"Create runtime [name]"} -func (b *BaseApi) CreateRuntime(c *gin.Context) { - var req request.RuntimeCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - ssl, err := runtimeService.Create(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, ssl) -} - -// @Tags Website -// @Summary Delete runtime -// @Description 删除运行环境 -// @Accept json -// @Param request body request.RuntimeDelete true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /runtimes/del [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除网站 [name]","formatEN":"Delete website [name]"} -func (b *BaseApi) DeleteRuntime(c *gin.Context) { - var req request.RuntimeDelete - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - err := runtimeService.Delete(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -func (b *BaseApi) DeleteRuntimeCheck(c *gin.Context) { - runTimeId, err := helper.GetIntParamByKey(c, "runTimeId") - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) - return - } - checkData, err := runtimeService.DeleteCheck(runTimeId) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, checkData) -} - -// @Tags Runtime -// @Summary Update runtime -// @Description 更新运行环境 -// @Accept json -// @Param request body request.RuntimeUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /runtimes/update [post] -// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新运行环境 [name]","formatEN":"Update runtime [name]"} -func (b *BaseApi) UpdateRuntime(c *gin.Context) { - var req request.RuntimeUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := runtimeService.Update(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags Runtime -// @Summary Get runtime -// @Description 获取运行环境 -// @Accept json -// @Param id path string true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /runtimes/:id [get] -func (b *BaseApi) GetRuntime(c *gin.Context) { - id, err := helper.GetIntParamByKey(c, "id") - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) - return - } - res, err := runtimeService.Get(id) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, res) -} - -// @Tags Runtime -// @Summary Get Node package scripts -// @Description 获取 Node 项目的 scripts -// @Accept json -// @Param request body request.NodePackageReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /runtimes/node/package [post] -func (b *BaseApi) GetNodePackageRunScript(c *gin.Context) { - var req request.NodePackageReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - res, err := runtimeService.GetNodePackageRunScript(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, res) -} - -// @Tags Runtime -// @Summary Operate runtime -// @Description 操作运行环境 -// @Accept json -// @Param request body request.RuntimeOperate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /runtimes/operate [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"操作运行环境 [name]","formatEN":"Operate runtime [name]"} -func (b *BaseApi) OperateRuntime(c *gin.Context) { - var req request.RuntimeOperate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - err := runtimeService.OperateRuntime(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags Runtime -// @Summary Get Node modules -// @Description 获取 Node 项目的 modules -// @Accept json -// @Param request body request.NodeModuleReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /runtimes/node/modules [post] -func (b *BaseApi) GetNodeModules(c *gin.Context) { - var req request.NodeModuleReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - res, err := runtimeService.GetNodeModules(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, res) -} - -// @Tags Runtime -// @Summary Operate Node modules -// @Description 操作 Node 项目 modules -// @Accept json -// @Param request body request.NodeModuleReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /runtimes/node/modules/operate [post] -func (b *BaseApi) OperateNodeModules(c *gin.Context) { - var req request.NodeModuleOperateReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - err := runtimeService.OperateNodeModules(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags Runtime -// @Summary Sync runtime status -// @Description 同步运行环境状态 -// @Accept json -// @Success 200 -// @Security ApiKeyAuth -// @Router /runtimes/sync [post] -func (b *BaseApi) SyncStatus(c *gin.Context) { - err := runtimeService.SyncRuntimeStatus() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} diff --git a/backend/app/api/v1/setting.go b/backend/app/api/v1/setting.go deleted file mode 100644 index 561882cf0..000000000 --- a/backend/app/api/v1/setting.go +++ /dev/null @@ -1,66 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/gin-gonic/gin" -) - -// @Tags System Setting -// @Summary Load system setting info -// @Description 加载系统配置信息 -// @Success 200 {object} dto.SettingInfo -// @Security ApiKeyAuth -// @Router /settings/search [post] -func (b *BaseApi) GetSettingInfo(c *gin.Context) { - setting, err := settingService.GetSettingInfo() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, setting) -} - -// @Tags System Setting -// @Summary Load system available status -// @Description 获取系统可用状态 -// @Success 200 -// @Security ApiKeyAuth -// @Router /settings/search/available [get] -func (b *BaseApi) GetSystemAvailable(c *gin.Context) { - helper.SuccessWithData(c, nil) -} - -// @Tags System Setting -// @Summary Update system setting -// @Description 更新系统配置 -// @Accept json -// @Param request body dto.SettingUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /settings/update [post] -// @x-panel-log {"bodyKeys":["key","value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改系统配置 [key] => [value]","formatEN":"update system setting [key] => [value]"} -func (b *BaseApi) UpdateSetting(c *gin.Context) { - var req dto.SettingUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := settingService.Update(req.Key, req.Value); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags System Setting -// @Summary Load local backup dir -// @Description 获取安装根目录 -// @Success 200 {string} path -// @Security ApiKeyAuth -// @Router /settings/basedir [get] -func (b *BaseApi) LoadBaseDir(c *gin.Context) { - helper.SuccessWithData(c, global.CONF.System.DataDir) -} diff --git a/backend/app/api/v1/snapshot.go b/backend/app/api/v1/snapshot.go deleted file mode 100644 index 3f79be8ac..000000000 --- a/backend/app/api/v1/snapshot.go +++ /dev/null @@ -1,187 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags System Setting -// @Summary Create system snapshot -// @Description 创建系统快照 -// @Accept json -// @Param request body dto.SnapshotCreate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /settings/snapshot [post] -// @x-panel-log {"bodyKeys":["from", "description"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建系统快照 [description] 到 [from]","formatEN":"Create system backup [description] to [from]"} -func (b *BaseApi) CreateSnapshot(c *gin.Context) { - var req dto.SnapshotCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := snapshotService.SnapshotCreate(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags System Setting -// @Summary Import system snapshot -// @Description 导入已有快照 -// @Accept json -// @Param request body dto.SnapshotImport true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /settings/snapshot/import [post] -// @x-panel-log {"bodyKeys":["from", "names"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"从 [from] 同步系统快照 [names]","formatEN":"Sync system snapshots [names] from [from]"} -func (b *BaseApi) ImportSnapshot(c *gin.Context) { - var req dto.SnapshotImport - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := snapshotService.SnapshotImport(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags System Setting -// @Summary Load Snapshot status -// @Description 获取快照状态 -// @Accept json -// @Param request body dto.OperateByID true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /settings/snapshot/status [post] -func (b *BaseApi) LoadSnapShotStatus(c *gin.Context) { - var req dto.OperateByID - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - data, err := snapshotService.LoadSnapShotStatus(req.ID) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, data) -} - -// @Tags System Setting -// @Summary Update snapshot description -// @Description 更新快照描述信息 -// @Accept json -// @Param request body dto.UpdateDescription true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /settings/snapshot/description/update [post] -// @x-panel-log {"bodyKeys":["id","description"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"snapshots","output_column":"name","output_value":"name"}],"formatZH":"快照 [name] 描述信息修改 [description]","formatEN":"The description of the snapshot [name] is modified => [description]"} -func (b *BaseApi) UpdateSnapDescription(c *gin.Context) { - var req dto.UpdateDescription - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := snapshotService.UpdateDescription(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags System Setting -// @Summary Page system snapshot -// @Description 获取系统快照列表分页 -// @Accept json -// @Param request body dto.SearchWithPage true "request" -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /settings/snapshot/search [post] -func (b *BaseApi) SearchSnapshot(c *gin.Context) { - var req dto.SearchWithPage - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - total, accounts, err := snapshotService.SearchWithPage(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, dto.PageResult{ - Total: total, - Items: accounts, - }) -} - -// @Tags System Setting -// @Summary Recover system backup -// @Description 从系统快照恢复 -// @Accept json -// @Param request body dto.SnapshotRecover true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /settings/snapshot/recover [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"snapshots","output_column":"name","output_value":"name"}],"formatZH":"从系统快照 [name] 恢复","formatEN":"Recover from system backup [name]"} -func (b *BaseApi) RecoverSnapshot(c *gin.Context) { - var req dto.SnapshotRecover - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := snapshotService.SnapshotRecover(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags System Setting -// @Summary Rollback system backup -// @Description 从系统快照回滚 -// @Accept json -// @Param request body dto.SnapshotRecover true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /settings/snapshot/rollback [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"snapshots","output_column":"name","output_value":"name"}],"formatZH":"从系统快照 [name] 回滚","formatEN":"Rollback from system backup [name]"} -func (b *BaseApi) RollbackSnapshot(c *gin.Context) { - var req dto.SnapshotRecover - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := snapshotService.SnapshotRollback(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags System Setting -// @Summary Delete system backup -// @Description 删除系统快照 -// @Accept json -// @Param request body dto.SnapshotBatchDelete true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /settings/snapshot/del [post] -// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"snapshots","output_column":"name","output_value":"name"}],"formatZH":"删除系统快照 [name]","formatEN":"Delete system backup [name]"} -func (b *BaseApi) DeleteSnapshot(c *gin.Context) { - var req dto.SnapshotBatchDelete - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := snapshotService.Delete(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} diff --git a/backend/app/api/v1/ssh.go b/backend/app/api/v1/ssh.go deleted file mode 100644 index b522a42b5..000000000 --- a/backend/app/api/v1/ssh.go +++ /dev/null @@ -1,169 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags SSH -// @Summary Load host SSH setting info -// @Description 加载 SSH 配置信息 -// @Success 200 {object} dto.SSHInfo -// @Security ApiKeyAuth -// @Router /host/ssh/search [post] -func (b *BaseApi) GetSSHInfo(c *gin.Context) { - info, err := sshService.GetSSHInfo() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, info) -} - -// @Tags SSH -// @Summary Operate SSH -// @Description 修改 SSH 服务状态 -// @Accept json -// @Param request body dto.Operate true "request" -// @Security ApiKeyAuth -// @Router /host/ssh/operate [post] -// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] SSH ","formatEN":"[operation] SSH"} -func (b *BaseApi) OperateSSH(c *gin.Context) { - var req dto.Operate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := sshService.OperateSSH(req.Operation); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags SSH -// @Summary Update host SSH setting -// @Description 更新 SSH 配置 -// @Accept json -// @Param request body dto.SSHUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /host/ssh/update [post] -// @x-panel-log {"bodyKeys":["key","value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 SSH 配置 [key] => [value]","formatEN":"update SSH setting [key] => [value]"} -func (b *BaseApi) UpdateSSH(c *gin.Context) { - var req dto.SSHUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := sshService.Update(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags SSH -// @Summary Update host SSH setting by file -// @Description 上传文件更新 SSH 配置 -// @Accept json -// @Param request body dto.SSHConf true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /host/conffile/update [post] -// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 SSH 配置文件","formatEN":"update SSH conf"} -func (b *BaseApi) UpdateSSHByfile(c *gin.Context) { - var req dto.SSHConf - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := sshService.UpdateByFile(req.File); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags SSH -// @Summary Generate host SSH secret -// @Description 生成 SSH 密钥 -// @Accept json -// @Param request body dto.GenerateSSH true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /host/ssh/generate [post] -// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成 SSH 密钥 ","formatEN":"generate SSH secret"} -func (b *BaseApi) GenerateSSH(c *gin.Context) { - var req dto.GenerateSSH - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := sshService.GenerateSSH(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags SSH -// @Summary Load host SSH secret -// @Description 获取 SSH 密钥 -// @Accept json -// @Param request body dto.GenerateLoad true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /host/ssh/secret [post] -func (b *BaseApi) LoadSSHSecret(c *gin.Context) { - var req dto.GenerateLoad - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - data, err := sshService.LoadSSHSecret(req.EncryptionMode) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, data) -} - -// @Tags SSH -// @Summary Load host SSH logs -// @Description 获取 SSH 登录日志 -// @Accept json -// @Param request body dto.SearchSSHLog true "request" -// @Success 200 {object} dto.SSHLog -// @Security ApiKeyAuth -// @Router /host/ssh/log [post] -func (b *BaseApi) LoadSSHLogs(c *gin.Context) { - var req dto.SearchSSHLog - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - data, err := sshService.LoadLog(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, data) -} - -// @Tags SSH -// @Summary Load host SSH conf -// @Description 获取 SSH 配置文件 -// @Success 200 -// @Security ApiKeyAuth -// @Router /host/ssh/conf [get] -func (b *BaseApi) LoadSSHConf(c *gin.Context) { - data, err := sshService.LoadSSHConf() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, data) -} diff --git a/backend/app/api/v1/terminal.go b/backend/app/api/v1/terminal.go deleted file mode 100644 index f79b66a3c..000000000 --- a/backend/app/api/v1/terminal.go +++ /dev/null @@ -1,285 +0,0 @@ -package v1 - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "net/http" - "strconv" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/copier" - "github.com/1Panel-dev/1Panel/backend/utils/ssh" - "github.com/1Panel-dev/1Panel/backend/utils/terminal" - "github.com/gin-gonic/gin" - "github.com/gorilla/websocket" - "github.com/pkg/errors" -) - -func (b *BaseApi) WsSsh(c *gin.Context) { - wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil) - if err != nil { - global.LOG.Errorf("gin context http handler failed, err: %v", err) - return - } - defer wsConn.Close() - - id, err := strconv.Atoi(c.Query("id")) - if wshandleError(wsConn, errors.WithMessage(err, "invalid param id in request")) { - return - } - cols, err := strconv.Atoi(c.DefaultQuery("cols", "80")) - if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) { - return - } - rows, err := strconv.Atoi(c.DefaultQuery("rows", "40")) - if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) { - return - } - host, err := hostService.GetHostInfo(uint(id)) - if wshandleError(wsConn, errors.WithMessage(err, "load host info by id failed")) { - return - } - var connInfo ssh.ConnInfo - _ = copier.Copy(&connInfo, &host) - connInfo.PrivateKey = []byte(host.PrivateKey) - if len(host.PassPhrase) != 0 { - connInfo.PassPhrase = []byte(host.PassPhrase) - } - - client, err := connInfo.NewClient() - if wshandleError(wsConn, errors.WithMessage(err, "failed to set up the connection. Please check the host information")) { - return - } - defer client.Close() - sws, err := terminal.NewLogicSshWsSession(cols, rows, true, connInfo.Client, wsConn) - if wshandleError(wsConn, err) { - return - } - defer sws.Close() - - quitChan := make(chan bool, 3) - sws.Start(quitChan) - go sws.Wait(quitChan) - - <-quitChan - - if wshandleError(wsConn, err) { - return - } -} - -func (b *BaseApi) RedisWsSsh(c *gin.Context) { - wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil) - if err != nil { - global.LOG.Errorf("gin context http handler failed, err: %v", err) - return - } - defer wsConn.Close() - - if global.CONF.System.IsDemo { - if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) { - return - } - } - - cols, err := strconv.Atoi(c.DefaultQuery("cols", "80")) - if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) { - return - } - rows, err := strconv.Atoi(c.DefaultQuery("rows", "40")) - if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) { - return - } - name := c.Query("name") - from := c.Query("from") - commands := []string{"redis-cli"} - database, err := databaseService.Get(name) - if wshandleError(wsConn, errors.WithMessage(err, "no such database in db")) { - return - } - if from == "local" { - redisInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: name, Type: "redis"}) - if wshandleError(wsConn, errors.WithMessage(err, "no such database in db")) { - return - } - name = redisInfo.ContainerName - if len(database.Password) != 0 { - commands = []string{"redis-cli", "-a", database.Password, "--no-auth-warning"} - } - } else { - itemPort := fmt.Sprintf("%v", database.Port) - commands = []string{"redis-cli", "-h", database.Address, "-p", itemPort} - if len(database.Password) != 0 { - commands = []string{"redis-cli", "-h", database.Address, "-p", itemPort, "-a", database.Password, "--no-auth-warning"} - } - name = "1Panel-redis-cli-tools" - } - - pidMap := loadMapFromDockerTop(name) - itemCmds := append([]string{"exec", "-it", name}, commands...) - slave, err := terminal.NewCommand(itemCmds) - if wshandleError(wsConn, err) { - return - } - defer killBash(name, strings.Join(commands, " "), pidMap) - defer slave.Close() - - tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false) - if wshandleError(wsConn, err) { - return - } - - quitChan := make(chan bool, 3) - tty.Start(quitChan) - go slave.Wait(quitChan) - - <-quitChan - - global.LOG.Info("websocket finished") - if wshandleError(wsConn, err) { - return - } -} - -func (b *BaseApi) ContainerWsSsh(c *gin.Context) { - wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil) - if err != nil { - global.LOG.Errorf("gin context http handler failed, err: %v", err) - return - } - defer wsConn.Close() - - if global.CONF.System.IsDemo { - if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) { - return - } - } - - containerID := c.Query("containerid") - command := c.Query("command") - user := c.Query("user") - if len(command) == 0 || len(containerID) == 0 { - if wshandleError(wsConn, errors.New("error param of command or containerID")) { - return - } - } - cols, err := strconv.Atoi(c.DefaultQuery("cols", "80")) - if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) { - return - } - rows, err := strconv.Atoi(c.DefaultQuery("rows", "40")) - if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) { - return - } - - cmds := []string{"exec", containerID, command} - if len(user) != 0 { - cmds = []string{"exec", "-u", user, containerID, command} - } - if cmd.CheckIllegal(user, containerID, command) { - if wshandleError(wsConn, errors.New(" The command contains illegal characters.")) { - return - } - } - stdout, err := cmd.ExecWithCheck("docker", cmds...) - if wshandleError(wsConn, errors.WithMessage(err, stdout)) { - return - } - - commands := []string{"exec", "-it", containerID, command} - if len(user) != 0 { - commands = []string{"exec", "-it", "-u", user, containerID, command} - } - pidMap := loadMapFromDockerTop(containerID) - slave, err := terminal.NewCommand(commands) - if wshandleError(wsConn, err) { - return - } - defer killBash(containerID, command, pidMap) - defer slave.Close() - - tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, true) - if wshandleError(wsConn, err) { - return - } - - quitChan := make(chan bool, 3) - tty.Start(quitChan) - go slave.Wait(quitChan) - - <-quitChan - - global.LOG.Info("websocket finished") - if wshandleError(wsConn, err) { - return - } -} - -func wshandleError(ws *websocket.Conn, err error) bool { - if err != nil { - global.LOG.Errorf("handler ws faled:, err: %v", err) - dt := time.Now().Add(time.Second) - if ctlerr := ws.WriteControl(websocket.CloseMessage, []byte(err.Error()), dt); ctlerr != nil { - wsData, err := json.Marshal(terminal.WsMsg{ - Type: terminal.WsMsgCmd, - Data: base64.StdEncoding.EncodeToString([]byte(err.Error())), - }) - if err != nil { - _ = ws.WriteMessage(websocket.TextMessage, []byte("{\"type\":\"cmd\",\"data\":\"failed to encoding to json\"}")) - } else { - _ = ws.WriteMessage(websocket.TextMessage, wsData) - } - } - return true - } - return false -} - -func loadMapFromDockerTop(containerID string) map[string]string { - pidMap := make(map[string]string) - sudo := cmd.SudoHandleCmd() - - stdout, err := cmd.Execf("%s docker top %s -eo pid,command ", sudo, containerID) - if err != nil { - return pidMap - } - lines := strings.Split(stdout, "\n") - for _, line := range lines { - parts := strings.Fields(line) - if len(parts) < 2 { - continue - } - pidMap[parts[0]] = strings.Join(parts[1:], " ") - } - return pidMap -} - -func killBash(containerID, comm string, pidMap map[string]string) { - sudo := cmd.SudoHandleCmd() - newPidMap := loadMapFromDockerTop(containerID) - for pid, command := range newPidMap { - isOld := false - for pid2 := range pidMap { - if pid == pid2 { - isOld = true - break - } - } - if !isOld && command == comm { - _, _ = cmd.Execf("%s kill -9 %s", sudo, pid) - } - } -} - -var upGrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024 * 1024 * 10, - CheckOrigin: func(r *http.Request) bool { - return true - }, -} diff --git a/backend/app/api/v1/website.go b/backend/app/api/v1/website.go deleted file mode 100644 index caef8f261..000000000 --- a/backend/app/api/v1/website.go +++ /dev/null @@ -1,822 +0,0 @@ -package v1 - -import ( - "encoding/base64" - - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Website -// @Summary Page websites -// @Description 获取网站列表分页 -// @Accept json -// @Param request body request.WebsiteSearch true "request" -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /websites/search [post] -func (b *BaseApi) PageWebsite(c *gin.Context) { - var req request.WebsiteSearch - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - total, websites, err := websiteService.PageWebsite(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, dto.PageResult{ - Total: total, - Items: websites, - }) -} - -// @Tags Website -// @Summary List websites -// @Description 获取网站列表 -// @Success 200 {array} response.WebsiteDTO -// @Security ApiKeyAuth -// @Router /websites/list [get] -func (b *BaseApi) GetWebsites(c *gin.Context) { - websites, err := websiteService.GetWebsites() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, websites) -} - -// @Tags Website -// @Summary List website names -// @Description 获取网站列表 -// @Success 200 {array} string -// @Security ApiKeyAuth -// @Router /websites/options [get] -func (b *BaseApi) GetWebsiteOptions(c *gin.Context) { - websites, err := websiteService.GetWebsiteOptions() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, websites) -} - -// @Tags Website -// @Summary Create website -// @Description 创建网站 -// @Accept json -// @Param request body request.WebsiteCreate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites [post] -// @x-panel-log {"bodyKeys":["primaryDomain"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建网站 [primaryDomain]","formatEN":"Create website [primaryDomain]"} -func (b *BaseApi) CreateWebsite(c *gin.Context) { - var req request.WebsiteCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if len(req.FtpPassword) != 0 { - pass, err := base64.StdEncoding.DecodeString(req.FtpPassword) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - req.FtpPassword = string(pass) - } - err := websiteService.CreateWebsite(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Website -// @Summary Operate website -// @Description 操作网站 -// @Accept json -// @Param request body request.WebsiteOp true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/operate [post] -// @x-panel-log {"bodyKeys":["id", "operate"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"[operate] 网站 [domain]","formatEN":"[operate] website [domain]"} -func (b *BaseApi) OpWebsite(c *gin.Context) { - var req request.WebsiteOp - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - err := websiteService.OpWebsite(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Website -// @Summary Delete website -// @Description 删除网站 -// @Accept json -// @Param request body request.WebsiteDelete true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/del [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"删除网站 [domain]","formatEN":"Delete website [domain]"} -func (b *BaseApi) DeleteWebsite(c *gin.Context) { - var req request.WebsiteDelete - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - err := websiteService.DeleteWebsite(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Website -// @Summary Update website -// @Description 更新网站 -// @Accept json -// @Param request body request.WebsiteUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/update [post] -// @x-panel-log {"bodyKeys":["primaryDomain"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新网站 [primaryDomain]","formatEN":"Update website [primaryDomain]"} -func (b *BaseApi) UpdateWebsite(c *gin.Context) { - var req request.WebsiteUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := websiteService.UpdateWebsite(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Website -// @Summary Search website by id -// @Description 通过 id 查询网站 -// @Accept json -// @Param id path integer true "request" -// @Success 200 {object} response.WebsiteDTO -// @Security ApiKeyAuth -// @Router /websites/:id [get] -func (b *BaseApi) GetWebsite(c *gin.Context) { - id, err := helper.GetParamID(c) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) - return - } - website, err := websiteService.GetWebsite(id) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, website) -} - -// @Tags Website Nginx -// @Summary Search website nginx by id -// @Description 通过 id 查询网站 nginx -// @Accept json -// @Param id path integer true "request" -// @Success 200 {object} response.FileInfo -// @Security ApiKeyAuth -// @Router /websites/:id/config/:type [get] -func (b *BaseApi) GetWebsiteNginx(c *gin.Context) { - id, err := helper.GetParamID(c) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) - return - } - configType := c.Param("type") - - fileInfo, err := websiteService.GetWebsiteNginxConfig(id, configType) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, fileInfo) -} - -// @Tags Website Nginx -// @Summary Load nginx conf -// @Description 获取 nginx 配置 -// @Accept json -// @Param request body request.NginxScopeReq true "request" -// @Success 200 {object} response.WebsiteNginxConfig -// @Security ApiKeyAuth -// @Router /websites/config [post] -func (b *BaseApi) GetNginxConfig(c *gin.Context) { - var req request.NginxScopeReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - config, err := websiteService.GetNginxConfigByScope(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, config) -} - -// @Tags Website Nginx -// @Summary Update nginx conf -// @Description 更新 nginx 配置 -// @Accept json -// @Param request body request.NginxConfigUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/config/update [post] -// @x-panel-log {"bodyKeys":["websiteId"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteId","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"nginx 配置修改 [domain]","formatEN":"Nginx conf update [domain]"} -func (b *BaseApi) UpdateNginxConfig(c *gin.Context) { - var req request.NginxConfigUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := websiteService.UpdateNginxConfigByScope(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Website HTTPS -// @Summary Load https conf -// @Description 获取 https 配置 -// @Accept json -// @Param id path integer true "request" -// @Success 200 {object} response.WebsiteHTTPS -// @Security ApiKeyAuth -// @Router /websites/:id/https [get] -func (b *BaseApi) GetHTTPSConfig(c *gin.Context) { - id, err := helper.GetParamID(c) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) - return - } - res, err := websiteService.GetWebsiteHTTPS(id) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, res) -} - -// @Tags Website HTTPS -// @Summary Update https conf -// @Description 更新 https 配置 -// @Accept json -// @Param request body request.WebsiteHTTPSOp true "request" -// @Success 200 {object} response.WebsiteHTTPS -// @Security ApiKeyAuth -// @Router /websites/:id/https [post] -// @x-panel-log {"bodyKeys":["websiteId"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteId","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"更新网站 [domain] https 配置","formatEN":"Update website https [domain] conf"} -func (b *BaseApi) UpdateHTTPSConfig(c *gin.Context) { - var req request.WebsiteHTTPSOp - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - tx, ctx := helper.GetTxAndContext() - res, err := websiteService.OpWebsiteHTTPS(ctx, req) - if err != nil { - tx.Rollback() - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - tx.Commit() - helper.SuccessWithData(c, res) -} - -// @Tags Website -// @Summary Check before create website -// @Description 网站创建前检查 -// @Accept json -// @Param request body request.WebsiteInstallCheckReq true "request" -// @Success 200 {array} response.WebsitePreInstallCheck -// @Security ApiKeyAuth -// @Router /websites/check [post] -func (b *BaseApi) CreateWebsiteCheck(c *gin.Context) { - var req request.WebsiteInstallCheckReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - data, err := websiteService.PreInstallCheck(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, data) -} - -// @Tags Website Nginx -// @Summary Update website nginx conf -// @Description 更新 网站 nginx 配置 -// @Accept json -// @Param request body request.WebsiteNginxUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/nginx/update [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"[domain] Nginx 配置修改","formatEN":"[domain] Nginx conf update"} -func (b *BaseApi) UpdateWebsiteNginxConfig(c *gin.Context) { - var req request.WebsiteNginxUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := websiteService.UpdateNginxConfigFile(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Website -// @Summary Operate website log -// @Description 操作网站日志 -// @Accept json -// @Param request body request.WebsiteLogReq true "request" -// @Success 200 {object} response.WebsiteLog -// @Security ApiKeyAuth -// @Router /websites/log [post] -// @x-panel-log {"bodyKeys":["id", "operate"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"[domain][operate] 日志","formatEN":"[domain][operate] logs"} -func (b *BaseApi) OpWebsiteLog(c *gin.Context) { - var req request.WebsiteLogReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - res, err := websiteService.OpWebsiteLog(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, res) -} - -// @Tags Website -// @Summary Change default server -// @Description 操作网站日志 -// @Accept json -// @Param request body request.WebsiteDefaultUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/default/server [post] -// @x-panel-log {"bodyKeys":["id", "operate"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"修改默认 server => [domain]","formatEN":"Change default server => [domain]"} -func (b *BaseApi) ChangeDefaultServer(c *gin.Context) { - var req request.WebsiteDefaultUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := websiteService.ChangeDefaultServer(req.ID); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Website -// @Summary Load website php conf -// @Description 获取网站 php 配置 -// @Accept json -// @Param id path integer true "request" -// @Success 200 {object} response.PHPConfig -// @Security ApiKeyAuth -// @Router /websites/php/config/:id [get] -func (b *BaseApi) GetWebsitePHPConfig(c *gin.Context) { - id, err := helper.GetParamID(c) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) - return - } - data, err := websiteService.GetPHPConfig(id) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, data) -} - -// @Tags Website PHP -// @Summary Update website php conf -// @Description 更新 网站 PHP 配置 -// @Accept json -// @Param request body request.WebsitePHPConfigUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/php/config [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"[domain] PHP 配置修改","formatEN":"[domain] PHP conf update"} -func (b *BaseApi) UpdateWebsitePHPConfig(c *gin.Context) { - var req request.WebsitePHPConfigUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := websiteService.UpdatePHPConfig(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Website PHP -// @Summary Update php conf -// @Description 更新 php 配置文件 -// @Accept json -// @Param request body request.WebsitePHPFileUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/php/update [post] -// @x-panel-log {"bodyKeys":["websiteId"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteId","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"php 配置修改 [domain]","formatEN":"Nginx conf update [domain]"} -func (b *BaseApi) UpdatePHPFile(c *gin.Context) { - var req request.WebsitePHPFileUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := websiteService.UpdatePHPConfigFile(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Website PHP -// @Summary Update php version -// @Description 变更 php 版本 -// @Accept json -// @Param request body request.WebsitePHPVersionReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/php/version [post] -// @x-panel-log {"bodyKeys":["websiteId"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteId","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"php 版本变更 [domain]","formatEN":"php version update [domain]"} -func (b *BaseApi) ChangePHPVersion(c *gin.Context) { - var req request.WebsitePHPVersionReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := websiteService.ChangePHPVersion(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags Website -// @Summary Get rewrite conf -// @Description 获取伪静态配置 -// @Accept json -// @Param request body request.NginxRewriteReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/rewrite [post] -func (b *BaseApi) GetRewriteConfig(c *gin.Context) { - var req request.NginxRewriteReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - res, err := websiteService.GetRewriteConfig(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, res) -} - -// @Tags Website -// @Summary Update rewrite conf -// @Description 更新伪静态配置 -// @Accept json -// @Param request body request.NginxRewriteUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/rewrite/update [post] -// @x-panel-log {"bodyKeys":["websiteID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteID","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"伪静态配置修改 [domain]","formatEN":"Nginx conf rewrite update [domain]"} -func (b *BaseApi) UpdateRewriteConfig(c *gin.Context) { - var req request.NginxRewriteUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := websiteService.UpdateRewriteConfig(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Website -// @Summary Update Site Dir -// @Description 更新网站目录 -// @Accept json -// @Param request body request.WebsiteUpdateDir true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/dir/update [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"更新网站 [domain] 目录","formatEN":"Update domain [domain] dir"} -func (b *BaseApi) UpdateSiteDir(c *gin.Context) { - var req request.WebsiteUpdateDir - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := websiteService.UpdateSiteDir(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags Website -// @Summary Update Site Dir permission -// @Description 更新网站目录权限 -// @Accept json -// @Param request body request.WebsiteUpdateDirPermission true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/dir/permission [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"更新网站 [domain] 目录权限","formatEN":"Update domain [domain] dir permission"} -func (b *BaseApi) UpdateSiteDirPermission(c *gin.Context) { - var req request.WebsiteUpdateDirPermission - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := websiteService.UpdateSitePermission(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags Website -// @Summary Get proxy conf -// @Description 获取反向代理配置 -// @Accept json -// @Param request body request.WebsiteProxyReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/proxies [post] -func (b *BaseApi) GetProxyConfig(c *gin.Context) { - var req request.WebsiteProxyReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - res, err := websiteService.GetProxies(req.ID) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, res) -} - -// @Tags Website -// @Summary Update proxy conf -// @Description 修改反向代理配置 -// @Accept json -// @Param request body request.WebsiteProxyConfig true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/proxies/update [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"修改网站 [domain] 反向代理配置 ","formatEN":"Update domain [domain] proxy config"} -func (b *BaseApi) UpdateProxyConfig(c *gin.Context) { - var req request.WebsiteProxyConfig - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - err := websiteService.OperateProxy(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags Website -// @Summary Update proxy file -// @Description 更新反向代理文件 -// @Accept json -// @Param request body request.NginxProxyUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/proxy/file [post] -// @x-panel-log {"bodyKeys":["websiteID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteID","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"更新反向代理文件 [domain]","formatEN":"Nginx conf proxy file update [domain]"} -func (b *BaseApi) UpdateProxyConfigFile(c *gin.Context) { - var req request.NginxProxyUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := websiteService.UpdateProxyFile(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags Website -// @Summary Get AuthBasic conf -// @Description 获取密码访问配置 -// @Accept json -// @Param request body request.NginxAuthReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/auths [post] -func (b *BaseApi) GetAuthConfig(c *gin.Context) { - var req request.NginxAuthReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - res, err := websiteService.GetAuthBasics(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, res) -} - -// @Tags Website -// @Summary Get AuthBasic conf -// @Description 更新密码访问配置 -// @Accept json -// @Param request body request.NginxAuthUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/auths/update [post] -func (b *BaseApi) UpdateAuthConfig(c *gin.Context) { - var req request.NginxAuthUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := websiteService.UpdateAuthBasic(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags Website -// @Summary Get AntiLeech conf -// @Description 获取防盗链配置 -// @Accept json -// @Param request body request.NginxCommonReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/leech [post] -func (b *BaseApi) GetAntiLeech(c *gin.Context) { - var req request.NginxCommonReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - res, err := websiteService.GetAntiLeech(req.WebsiteID) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, res) -} - -// @Tags Website -// @Summary Update AntiLeech -// @Description 更新防盗链配置 -// @Accept json -// @Param request body request.NginxAntiLeechUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/leech/update [post] -func (b *BaseApi) UpdateAntiLeech(c *gin.Context) { - var req request.NginxAntiLeechUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := websiteService.UpdateAntiLeech(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags Website -// @Summary Update redirect conf -// @Description 修改重定向配置 -// @Accept json -// @Param request body request.NginxRedirectReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/redirect/update [post] -// @x-panel-log {"bodyKeys":["websiteID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteID","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"修改网站 [domain] 重定向理配置 ","formatEN":"Update domain [domain] redirect config"} -func (b *BaseApi) UpdateRedirectConfig(c *gin.Context) { - var req request.NginxRedirectReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - err := websiteService.OperateRedirect(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags Website -// @Summary Get redirect conf -// @Description 获取重定向配置 -// @Accept json -// @Param request body request.WebsiteProxyReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/redirect [post] -func (b *BaseApi) GetRedirectConfig(c *gin.Context) { - var req request.WebsiteRedirectReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - res, err := websiteService.GetRedirect(req.WebsiteID) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, res) -} - -// @Tags Website -// @Summary Update redirect file -// @Description 更新重定向文件 -// @Accept json -// @Param request body request.NginxRedirectUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/redirect/file [post] -// @x-panel-log {"bodyKeys":["websiteID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteID","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"更新重定向文件 [domain]","formatEN":"Nginx conf redirect file update [domain]"} -func (b *BaseApi) UpdateRedirectConfigFile(c *gin.Context) { - var req request.NginxRedirectUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := websiteService.UpdateRedirectFile(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags Website -// @Summary Get website dir -// @Description 获取网站目录配置 -// @Accept json -// @Param request body request.WebsiteCommonReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/dir [post] -func (b *BaseApi) GetDirConfig(c *gin.Context) { - var req request.WebsiteCommonReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - res, err := websiteService.LoadWebsiteDirConfig(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, res) -} - -// @Tags Website -// @Summary Get default html -// @Description 获取默认 html -// @Accept json -// @Success 200 {object} response.FileInfo -// @Security ApiKeyAuth -// @Router /websites/default/html/:type [get] -func (b *BaseApi) GetDefaultHtml(c *gin.Context) { - resourceType, err := helper.GetStrParamByKey(c, "type") - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) - return - } - fileInfo, err := websiteService.GetDefaultHtml(resourceType) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, fileInfo) -} - -// @Tags Website -// @Summary Update default html -// @Description 更新默认 html -// @Accept json -// @Param request body request.WebsiteHtmlUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/default/html/update [post] -// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新默认 html","formatEN":"Update default html"} -func (b *BaseApi) UpdateDefaultHtml(c *gin.Context) { - var req request.WebsiteHtmlUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := websiteService.UpdateDefaultHtml(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} diff --git a/backend/app/api/v1/website_acme_account.go b/backend/app/api/v1/website_acme_account.go deleted file mode 100644 index 8360edb52..000000000 --- a/backend/app/api/v1/website_acme_account.go +++ /dev/null @@ -1,76 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Website Acme -// @Summary Page website acme accounts -// @Description 获取网站 acme 列表分页 -// @Accept json -// @Param request body dto.PageInfo true "request" -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /websites/acme/search [post] -func (b *BaseApi) PageWebsiteAcmeAccount(c *gin.Context) { - var req dto.PageInfo - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - total, accounts, err := websiteAcmeAccountService.Page(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, dto.PageResult{ - Total: total, - Items: accounts, - }) -} - -// @Tags Website Acme -// @Summary Create website acme account -// @Description 创建网站 acme -// @Accept json -// @Param request body request.WebsiteAcmeAccountCreate true "request" -// @Success 200 {object} response.WebsiteAcmeAccountDTO -// @Security ApiKeyAuth -// @Router /websites/acme [post] -// @x-panel-log {"bodyKeys":["email"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建网站 acme [email]","formatEN":"Create website acme [email]"} -func (b *BaseApi) CreateWebsiteAcmeAccount(c *gin.Context) { - var req request.WebsiteAcmeAccountCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - res, err := websiteAcmeAccountService.Create(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, res) -} - -// @Tags Website Acme -// @Summary Delete website acme account -// @Description 删除网站 acme -// @Accept json -// @Param request body request.WebsiteResourceReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/acme/del [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_acme_accounts","output_column":"email","output_value":"email"}],"formatZH":"删除网站 acme [email]","formatEN":"Delete website acme [email]"} -func (b *BaseApi) DeleteWebsiteAcmeAccount(c *gin.Context) { - var req request.WebsiteResourceReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := websiteAcmeAccountService.Delete(req.ID); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} diff --git a/backend/app/api/v1/website_ca.go b/backend/app/api/v1/website_ca.go deleted file mode 100644 index 986b6fa9f..000000000 --- a/backend/app/api/v1/website_ca.go +++ /dev/null @@ -1,177 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" - "net/http" - "net/url" - "strconv" -) - -// @Tags Website CA -// @Summary Page website ca -// @Description 获取网站 ca 列表分页 -// @Accept json -// @Param request body request.WebsiteCASearch true "request" -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /websites/ca/search [post] -func (b *BaseApi) PageWebsiteCA(c *gin.Context) { - var req request.WebsiteCASearch - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - total, cas, err := websiteCAService.Page(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, dto.PageResult{ - Total: total, - Items: cas, - }) -} - -// @Tags Website CA -// @Summary Create website ca -// @Description 创建网站 ca -// @Accept json -// @Param request body request.WebsiteCACreate true "request" -// @Success 200 {object} request.WebsiteCACreate -// @Security ApiKeyAuth -// @Router /websites/ca [post] -// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建网站 ca [name]","formatEN":"Create website ca [name]"} -func (b *BaseApi) CreateWebsiteCA(c *gin.Context) { - var req request.WebsiteCACreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - res, err := websiteCAService.Create(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, res) -} - -// @Tags Website CA -// @Summary Get website ca -// @Description 获取网站 ca -// @Accept json -// @Param id path int true "id" -// @Success 200 {object} response.WebsiteCADTO -// @Security ApiKeyAuth -// @Router /websites/ca/{id} [get] -func (b *BaseApi) GetWebsiteCA(c *gin.Context) { - id, err := helper.GetParamID(c) - if err != nil { - return - } - res, err := websiteCAService.GetCA(id) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, res) -} - -// @Tags Website CA -// @Summary Delete website ca -// @Description 删除网站 ca -// @Accept json -// @Param request body request.WebsiteCommonReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/ca/del [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_cas","output_column":"name","output_value":"name"}],"formatZH":"删除网站 ca [name]","formatEN":"Delete website ca [name]"} -func (b *BaseApi) DeleteWebsiteCA(c *gin.Context) { - var req request.WebsiteCommonReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := websiteCAService.Delete(req.ID); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags Website CA -// @Summary Obtain SSL -// @Description 自签 SSL 证书 -// @Accept json -// @Param request body request.WebsiteCAObtain true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/ca/obtain [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_cas","output_column":"name","output_value":"name"}],"formatZH":"自签 SSL 证书 [name]","formatEN":"Obtain SSL [name]"} -func (b *BaseApi) ObtainWebsiteCA(c *gin.Context) { - var req request.WebsiteCAObtain - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if _, err := websiteCAService.ObtainSSL(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags Website CA -// @Summary Obtain SSL -// @Description 续签 SSL 证书 -// @Accept json -// @Param request body request.WebsiteCAObtain true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/ca/renew [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_cas","output_column":"name","output_value":"name"}],"formatZH":"自签 SSL 证书 [name]","formatEN":"Obtain SSL [name]"} -func (b *BaseApi) RenewWebsiteCA(c *gin.Context) { - var req request.WebsiteCARenew - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if _, err := websiteCAService.ObtainSSL(request.WebsiteCAObtain{ - SSLID: req.SSLID, - Renew: true, - Unit: "year", - Time: 1, - }); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags Website CA -// @Summary Download CA file -// @Description 下载 CA 证书文件 -// @Accept json -// @Param request body request.WebsiteResourceReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/ca/download [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_cas","output_column":"name","output_value":"name"}],"formatZH":"下载 CA 证书文件 [name]","formatEN":"download ca file [name]"} -func (b *BaseApi) DownloadCAFile(c *gin.Context) { - var req request.WebsiteResourceReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - file, err := websiteCAService.DownloadFile(req.ID) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - defer file.Close() - info, err := file.Stat() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - c.Header("Content-Length", strconv.FormatInt(info.Size(), 10)) - c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(info.Name())) - http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), file) -} diff --git a/backend/app/api/v1/website_dns_account.go b/backend/app/api/v1/website_dns_account.go deleted file mode 100644 index 09ecdb7da..000000000 --- a/backend/app/api/v1/website_dns_account.go +++ /dev/null @@ -1,96 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Website DNS -// @Summary Page website dns accounts -// @Description 获取网站 dns 列表分页 -// @Accept json -// @Param request body dto.PageInfo true "request" -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /websites/dns/search [post] -func (b *BaseApi) PageWebsiteDnsAccount(c *gin.Context) { - var req dto.PageInfo - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - total, accounts, err := websiteDnsAccountService.Page(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, dto.PageResult{ - Total: total, - Items: accounts, - }) -} - -// @Tags Website DNS -// @Summary Create website dns account -// @Description 创建网站 dns -// @Accept json -// @Param request body request.WebsiteDnsAccountCreate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/dns [post] -// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建网站 dns [name]","formatEN":"Create website dns [name]"} -func (b *BaseApi) CreateWebsiteDnsAccount(c *gin.Context) { - var req request.WebsiteDnsAccountCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if _, err := websiteDnsAccountService.Create(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Website DNS -// @Summary Update website dns account -// @Description 更新网站 dns -// @Accept json -// @Param request body request.WebsiteDnsAccountUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/dns/update [post] -// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新网站 dns [name]","formatEN":"Update website dns [name]"} -func (b *BaseApi) UpdateWebsiteDnsAccount(c *gin.Context) { - var req request.WebsiteDnsAccountUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if _, err := websiteDnsAccountService.Update(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Website DNS -// @Summary Delete website dns account -// @Description 删除网站 dns -// @Accept json -// @Param request body request.WebsiteResourceReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/dns/del [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_dns_accounts","output_column":"name","output_value":"name"}],"formatZH":"删除网站 dns [name]","formatEN":"Delete website dns [name]"} -func (b *BaseApi) DeleteWebsiteDnsAccount(c *gin.Context) { - var req request.WebsiteResourceReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := websiteDnsAccountService.Delete(req.ID); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} diff --git a/backend/app/api/v1/website_domain.go b/backend/app/api/v1/website_domain.go deleted file mode 100644 index d50433435..000000000 --- a/backend/app/api/v1/website_domain.go +++ /dev/null @@ -1,73 +0,0 @@ -package v1 - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Website Domain -// @Summary Delete website domain -// @Description 删除网站域名 -// @Accept json -// @Param request body request.WebsiteDomainDelete true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/domains/del [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_domains","output_column":"domain","output_value":"domain"}],"formatZH":"删除域名 [domain]","formatEN":"Delete domain [domain]"} -func (b *BaseApi) DeleteWebDomain(c *gin.Context) { - var req request.WebsiteDomainDelete - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := websiteService.DeleteWebsiteDomain(req.ID); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Website Domain -// @Summary Create website domain -// @Description 创建网站域名 -// @Accept json -// @Param request body request.WebsiteDomainCreate true "request" -// @Success 200 {object} model.WebsiteDomain -// @Security ApiKeyAuth -// @Router /websites/domains [post] -// @x-panel-log {"bodyKeys":["domain"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建域名 [domain]","formatEN":"Create domain [domain]"} -func (b *BaseApi) CreateWebDomain(c *gin.Context) { - var req request.WebsiteDomainCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - domain, err := websiteService.CreateWebsiteDomain(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, domain) -} - -// @Tags Website Domain -// @Summary Search website domains by websiteId -// @Description 通过网站 id 查询域名 -// @Accept json -// @Param websiteId path integer true "request" -// @Success 200 {array} model.WebsiteDomain -// @Security ApiKeyAuth -// @Router /websites/domains/:websiteId [get] -func (b *BaseApi) GetWebDomains(c *gin.Context) { - websiteId, err := helper.GetIntParamByKey(c, "websiteId") - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) - return - } - list, err := websiteService.GetWebsiteDomain(websiteId) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, list) -} diff --git a/backend/app/api/v1/website_ssl.go b/backend/app/api/v1/website_ssl.go deleted file mode 100644 index d0420087d..000000000 --- a/backend/app/api/v1/website_ssl.go +++ /dev/null @@ -1,248 +0,0 @@ -package v1 - -import ( - "net/http" - "net/url" - "reflect" - "strconv" - - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -// @Tags Website SSL -// @Summary Page website ssl -// @Description 获取网站 ssl 列表分页 -// @Accept json -// @Param request body request.WebsiteSSLSearch true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/ssl/search [post] -func (b *BaseApi) PageWebsiteSSL(c *gin.Context) { - var req request.WebsiteSSLSearch - if err := helper.CheckBind(&req, c); err != nil { - return - } - if !reflect.DeepEqual(req.PageInfo, dto.PageInfo{}) { - total, accounts, err := websiteSSLService.Page(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, dto.PageResult{ - Total: total, - Items: accounts, - }) - } else { - list, err := websiteSSLService.Search(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, list) - } -} - -// @Tags Website SSL -// @Summary Create website ssl -// @Description 创建网站 ssl -// @Accept json -// @Param request body request.WebsiteSSLCreate true "request" -// @Success 200 {object} request.WebsiteSSLCreate -// @Security ApiKeyAuth -// @Router /websites/ssl [post] -// @x-panel-log {"bodyKeys":["primaryDomain"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建网站 ssl [primaryDomain]","formatEN":"Create website ssl [primaryDomain]"} -func (b *BaseApi) CreateWebsiteSSL(c *gin.Context) { - var req request.WebsiteSSLCreate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - res, err := websiteSSLService.Create(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, res) -} - -// @Tags Website SSL -// @Summary Apply ssl -// @Description 申请证书 -// @Accept json -// @Param request body request.WebsiteSSLApply true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/ssl/obtain [post] -// @x-panel-log {"bodyKeys":["ID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ID","isList":false,"db":"website_ssls","output_column":"primary_domain","output_value":"domain"}],"formatZH":"申请证书 [domain]","formatEN":"apply ssl [domain]"} -func (b *BaseApi) ApplyWebsiteSSL(c *gin.Context) { - var req request.WebsiteSSLApply - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := websiteSSLService.ObtainSSL(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithOutData(c) -} - -// @Tags Website SSL -// @Summary Resolve website ssl -// @Description 解析网站 ssl -// @Accept json -// @Param request body request.WebsiteDNSReq true "request" -// @Success 200 {array} response.WebsiteDNSRes -// @Security ApiKeyAuth -// @Router /websites/ssl/resolve [post] -func (b *BaseApi) GetDNSResolve(c *gin.Context) { - var req request.WebsiteDNSReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - res, err := websiteSSLService.GetDNSResolve(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, res) -} - -// @Tags Website SSL -// @Summary Delete website ssl -// @Description 删除网站 ssl -// @Accept json -// @Param request body request.WebsiteBatchDelReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/ssl/del [post] -// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"website_ssls","output_column":"primary_domain","output_value":"domain"}],"formatZH":"删除 ssl [domain]","formatEN":"Delete ssl [domain]"} -func (b *BaseApi) DeleteWebsiteSSL(c *gin.Context) { - var req request.WebsiteBatchDelReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := websiteSSLService.Delete(req.IDs); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Website SSL -// @Summary Search website ssl by website id -// @Description 通过网站 id 查询 ssl -// @Accept json -// @Param websiteId path integer true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/ssl/website/:websiteId [get] -func (b *BaseApi) GetWebsiteSSLByWebsiteId(c *gin.Context) { - websiteId, err := helper.GetIntParamByKey(c, "websiteId") - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - websiteSSL, err := websiteSSLService.GetWebsiteSSL(websiteId) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, websiteSSL) -} - -// @Tags Website SSL -// @Summary Search website ssl by id -// @Description 通过 id 查询 ssl -// @Accept json -// @Param id path integer true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/ssl/:id [get] -func (b *BaseApi) GetWebsiteSSLById(c *gin.Context) { - id, err := helper.GetParamID(c) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) - return - } - websiteSSL, err := websiteSSLService.GetSSL(id) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, websiteSSL) -} - -// @Tags Website SSL -// @Summary Update ssl -// @Description 更新 ssl -// @Accept json -// @Param request body request.WebsiteSSLUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/ssl/update [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_ssls","output_column":"primary_domain","output_value":"domain"}],"formatZH":"更新证书设置 [domain]","formatEN":"Update ssl config [domain]"} -func (b *BaseApi) UpdateWebsiteSSL(c *gin.Context) { - var req request.WebsiteSSLUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := websiteSSLService.Update(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Website SSL -// @Summary Upload ssl -// @Description 上传 ssl -// @Accept json -// @Param request body request.WebsiteSSLUpload true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/ssl/upload [post] -// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"上传 ssl [type]","formatEN":"Upload ssl [type]"} -func (b *BaseApi) UploadWebsiteSSL(c *gin.Context) { - var req request.WebsiteSSLUpload - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - if err := websiteSSLService.Upload(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Website SSL -// @Summary Download SSL file -// @Description 下载证书文件 -// @Accept json -// @Param request body request.WebsiteResourceReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /websites/ssl/download [post] -// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_ssls","output_column":"primary_domain","output_value":"domain"}],"formatZH":"下载证书文件 [domain]","formatEN":"download ssl file [domain]"} -func (b *BaseApi) DownloadWebsiteSSL(c *gin.Context) { - var req request.WebsiteResourceReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - file, err := websiteSSLService.DownloadFile(req.ID) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - defer file.Close() - info, err := file.Stat() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - c.Header("Content-Length", strconv.FormatInt(info.Size(), 10)) - c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(info.Name())) - http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), file) -} diff --git a/backend/app/dto/app.go b/backend/app/dto/app.go deleted file mode 100644 index 73b075c91..000000000 --- a/backend/app/dto/app.go +++ /dev/null @@ -1,149 +0,0 @@ -package dto - -import ( - "github.com/1Panel-dev/1Panel/backend/app/model" -) - -type AppDatabase struct { - ServiceName string `json:"PANEL_DB_HOST"` - DbName string `json:"PANEL_DB_NAME"` - DbUser string `json:"PANEL_DB_USER"` - Password string `json:"PANEL_DB_USER_PASSWORD"` -} - -type AuthParam struct { - RootPassword string `json:"PANEL_DB_ROOT_PASSWORD"` - RootUser string `json:"PANEL_DB_ROOT_USER"` -} - -type RedisAuthParam struct { - RootPassword string `json:"PANEL_REDIS_ROOT_PASSWORD"` -} - -type MinioAuthParam struct { - RootPassword string `json:"PANEL_MINIO_ROOT_PASSWORD"` - RootUser string `json:"PANEL_MINIO_ROOT_USER"` -} - -type ContainerExec struct { - ContainerName string `json:"containerName"` - DbParam AppDatabase `json:"dbParam"` - Auth AuthParam `json:"auth"` -} - -type AppOssConfig struct { - Version string `json:"version"` - Package string `json:"package"` -} - -type AppVersion struct { - Version string `json:"version"` - DetailId uint `json:"detailId"` - DockerCompose string `json:"dockerCompose"` -} - -type AppList struct { - Valid bool `json:"valid"` - Violations []string `json:"violations"` - LastModified int `json:"lastModified"` - - Apps []AppDefine `json:"apps"` - Extra ExtraProperties `json:"additionalProperties"` -} - -type AppDefine struct { - Icon string `json:"icon"` - Name string `json:"name"` - ReadMe string `json:"readMe"` - LastModified int `json:"lastModified"` - - AppProperty AppProperty `json:"additionalProperties"` - Versions []AppConfigVersion `json:"versions"` -} - -type LocalAppAppDefine struct { - AppProperty model.App `json:"additionalProperties" yaml:"additionalProperties"` -} - -type LocalAppParam struct { - AppParams LocalAppInstallDefine `json:"additionalProperties" yaml:"additionalProperties"` -} - -type LocalAppInstallDefine struct { - FormFields interface{} `json:"formFields" yaml:"formFields"` -} - -type ExtraProperties struct { - Tags []Tag `json:"tags"` - Version string `json:"version"` -} - -type AppProperty struct { - Name string `json:"name"` - Type string `json:"type"` - Tags []string `json:"tags"` - ShortDescZh string `json:"shortDescZh"` - ShortDescEn string `json:"shortDescEn"` - Key string `json:"key"` - Required []string `json:"Required"` - CrossVersionUpdate bool `json:"crossVersionUpdate"` - Limit int `json:"limit"` - Recommend int `json:"recommend"` - Website string `json:"website"` - Github string `json:"github"` - Document string `json:"document"` -} - -type AppConfigVersion struct { - Name string `json:"name"` - LastModified int `json:"lastModified"` - DownloadUrl string `json:"downloadUrl"` - DownloadCallBackUrl string `json:"downloadCallBackUrl"` - AppForm interface{} `json:"additionalProperties"` -} - -type Tag struct { - Key string `json:"key"` - Name string `json:"name"` - Sort int `json:"sort"` -} - -type AppForm struct { - FormFields []AppFormFields `json:"formFields"` -} - -type AppFormFields struct { - Type string `json:"type"` - LabelZh string `json:"labelZh"` - LabelEn string `json:"labelEn"` - Required bool `json:"required"` - Default interface{} `json:"default"` - EnvKey string `json:"envKey"` - Disabled bool `json:"disabled"` - Edit bool `json:"edit"` - Rule string `json:"rule"` - Multiple bool `json:"multiple"` - Child interface{} `json:"child"` - Values []AppFormValue `json:"values"` -} - -type AppFormValue struct { - Label string `json:"label"` - Value string `json:"value"` -} - -type AppResource struct { - Type string `json:"type"` - Name string `json:"name"` -} - -var AppToolMap = map[string]string{ - "mysql": "phpmyadmin", - "redis": "redis-commander", -} - -type AppInstallInfo struct { - ID uint `json:"id"` - Key string `json:"key"` - Name string `json:"name"` -} diff --git a/backend/app/dto/backup.go b/backend/app/dto/backup.go deleted file mode 100644 index 971c06519..000000000 --- a/backend/app/dto/backup.go +++ /dev/null @@ -1,82 +0,0 @@ -package dto - -import "time" - -type BackupOperate struct { - ID uint `json:"id"` - Type string `json:"type" validate:"required"` - Bucket string `json:"bucket"` - AccessKey string `json:"accessKey"` - Credential string `json:"credential"` - BackupPath string `json:"backupPath"` - Vars string `json:"vars" validate:"required"` -} - -type BackupInfo struct { - ID uint `json:"id"` - CreatedAt time.Time `json:"createdAt"` - Type string `json:"type"` - Bucket string `json:"bucket"` - BackupPath string `json:"backupPath"` - Vars string `json:"vars"` -} - -type OneDriveInfo struct { - ClientID string `json:"client_id"` - ClientSecret string `json:"client_secret"` - RedirectUri string `json:"redirect_uri"` -} - -type BackupSearchFile struct { - Type string `json:"type" validate:"required"` -} - -type CommonBackup struct { - Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql"` - Name string `json:"name"` - DetailName string `json:"detailName"` - Secret string `json:"secret"` -} -type CommonRecover struct { - Source string `json:"source" validate:"required,oneof=OSS S3 SFTP MINIO LOCAL COS KODO OneDrive WebDAV"` - Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql"` - Name string `json:"name"` - DetailName string `json:"detailName"` - File string `json:"file"` - Secret string `json:"secret"` -} - -type RecordSearch struct { - PageInfo - Type string `json:"type" validate:"required"` - Name string `json:"name"` - DetailName string `json:"detailName"` -} - -type RecordSearchByCronjob struct { - PageInfo - CronjobID uint `json:"cronjobID" validate:"required"` -} - -type BackupRecords struct { - ID uint `json:"id"` - CreatedAt time.Time `json:"createdAt"` - Source string `json:"source"` - BackupType string `json:"backupType"` - FileDir string `json:"fileDir"` - FileName string `json:"fileName"` - Size int64 `json:"size"` -} - -type DownloadRecord struct { - Source string `json:"source" validate:"required,oneof=OSS S3 SFTP MINIO LOCAL COS KODO OneDrive WebDAV"` - FileDir string `json:"fileDir" validate:"required"` - FileName string `json:"fileName" validate:"required"` -} - -type ForBuckets struct { - Type string `json:"type" validate:"required"` - AccessKey string `json:"accessKey"` - Credential string `json:"credential" validate:"required"` - Vars string `json:"vars" validate:"required"` -} diff --git a/backend/app/dto/clam.go b/backend/app/dto/clam.go deleted file mode 100644 index 964db15ee..000000000 --- a/backend/app/dto/clam.go +++ /dev/null @@ -1,96 +0,0 @@ -package dto - -import ( - "time" -) - -type SearchClamWithPage struct { - PageInfo - Info string `json:"info"` - OrderBy string `json:"orderBy" validate:"required,oneof=name status created_at"` - Order string `json:"order" validate:"required,oneof=null ascending descending"` -} - -type ClamBaseInfo struct { - Version string `json:"version"` - IsActive bool `json:"isActive"` - IsExist bool `json:"isExist"` - - FreshVersion string `json:"freshVersion"` - FreshIsActive bool `json:"freshIsActive"` - FreshIsExist bool `json:"freshIsExist"` -} - -type ClamInfo struct { - ID uint `json:"id"` - CreatedAt time.Time `json:"createdAt"` - - Name string `json:"name"` - Status string `json:"status"` - Path string `json:"path"` - InfectedStrategy string `json:"infectedStrategy"` - InfectedDir string `json:"infectedDir"` - LastHandleDate string `json:"lastHandleDate"` - Spec string `json:"spec"` - Description string `json:"description"` -} - -type ClamLogSearch struct { - PageInfo - - ClamID uint `json:"clamID"` - StartTime time.Time `json:"startTime"` - EndTime time.Time `json:"endTime"` -} - -type ClamLogReq struct { - Tail string `json:"tail"` - ClamName string `json:"clamName"` - RecordName string `json:"recordName"` -} - -type ClamFileReq struct { - Tail string `json:"tail"` - Name string `json:"name" validate:"required"` -} - -type ClamLog struct { - Name string `json:"name"` - ScanDate string `json:"scanDate"` - ScanTime string `json:"scanTime"` - InfectedFiles string `json:"infectedFiles"` - TotalError string `json:"totalError"` - Status string `json:"status"` -} - -type ClamCreate struct { - Name string `json:"name"` - Status string `json:"status"` - Path string `json:"path"` - InfectedStrategy string `json:"infectedStrategy"` - InfectedDir string `json:"infectedDir"` - Spec string `json:"spec"` - Description string `json:"description"` -} - -type ClamUpdate struct { - ID uint `json:"id"` - - Name string `json:"name"` - Path string `json:"path"` - InfectedStrategy string `json:"infectedStrategy"` - InfectedDir string `json:"infectedDir"` - Spec string `json:"spec"` - Description string `json:"description"` -} - -type ClamUpdateStatus struct { - ID uint `json:"id"` - Status string `json:"status"` -} - -type ClamDelete struct { - RemoveRecord bool `json:"removeRecord"` - RemoveInfected bool `json:"removeInfected"` - Ids []uint `json:"ids" validate:"required"` -} diff --git a/backend/app/dto/command.go b/backend/app/dto/command.go deleted file mode 100644 index b085ce66f..000000000 --- a/backend/app/dto/command.go +++ /dev/null @@ -1,38 +0,0 @@ -package dto - -type SearchCommandWithPage struct { - PageInfo - OrderBy string `json:"orderBy" validate:"required,oneof=name command created_at"` - Order string `json:"order" validate:"required,oneof=null ascending descending"` - GroupID uint `json:"groupID"` - Info string `json:"info"` - Name string `json:"name"` -} - -type CommandOperate struct { - ID uint `json:"id"` - GroupID uint `json:"groupID"` - GroupBelong string `json:"groupBelong"` - Name string `json:"name" validate:"required"` - Command string `json:"command" validate:"required"` -} - -type CommandInfo struct { - ID uint `json:"id"` - GroupID uint `json:"groupID"` - Name string `json:"name"` - Command string `json:"command"` - GroupBelong string `json:"groupBelong"` -} - -type CommandTree struct { - ID uint `json:"id"` - Label string `json:"label"` - Children []CommandInfo `json:"children"` -} - -type RedisCommand struct { - ID uint `json:"id"` - Name string `json:"name"` - Command string `json:"command"` -} diff --git a/backend/app/dto/common_req.go b/backend/app/dto/common_req.go deleted file mode 100644 index b493fac55..000000000 --- a/backend/app/dto/common_req.go +++ /dev/null @@ -1,54 +0,0 @@ -package dto - -type SearchWithPage struct { - PageInfo - Info string `json:"info"` -} - -type PageInfo struct { - Page int `json:"page" validate:"required,number"` - PageSize int `json:"pageSize" validate:"required,number"` -} - -type UpdateDescription struct { - ID uint `json:"id" validate:"required"` - Description string `json:"description" validate:"max=256"` -} - -type OperationWithName struct { - Name string `json:"name" validate:"required"` -} - -type OperateByID struct { - ID uint `json:"id" validate:"required"` -} - -type Operate struct { - Operation string `json:"operation" validate:"required"` -} - -type BatchDeleteReq struct { - Ids []uint `json:"ids" validate:"required"` -} - -type FilePath struct { - Path string `json:"path" validate:"required"` -} - -type DeleteByName struct { - Name string `json:"name" validate:"required"` -} - -type UpdateByFile struct { - File string `json:"file"` -} - -type UpdateByNameAndFile struct { - Name string `json:"name"` - File string `json:"file"` -} - -type OperationWithNameAndType struct { - Name string `json:"name"` - Type string `json:"type" validate:"required"` -} diff --git a/backend/app/dto/common_res.go b/backend/app/dto/common_res.go deleted file mode 100644 index 5c7c11193..000000000 --- a/backend/app/dto/common_res.go +++ /dev/null @@ -1,16 +0,0 @@ -package dto - -type PageResult struct { - Total int64 `json:"total"` - Items interface{} `json:"items"` -} - -type Response struct { - Code int `json:"code"` - Message string `json:"message"` - Data interface{} `json:"data"` -} - -type Options struct { - Option string `json:"option"` -} diff --git a/backend/app/dto/compose_template.go b/backend/app/dto/compose_template.go deleted file mode 100644 index de3832ab0..000000000 --- a/backend/app/dto/compose_template.go +++ /dev/null @@ -1,23 +0,0 @@ -package dto - -import "time" - -type ComposeTemplateCreate struct { - Name string `json:"name" validate:"required"` - Description string `json:"description"` - Content string `json:"content"` -} - -type ComposeTemplateUpdate struct { - ID uint `json:"id"` - Description string `json:"description"` - Content string `json:"content"` -} - -type ComposeTemplateInfo struct { - ID uint `json:"id"` - CreatedAt time.Time `json:"createdAt"` - Name string `json:"name"` - Description string `json:"description"` - Content string `json:"content"` -} diff --git a/backend/app/dto/container.go b/backend/app/dto/container.go deleted file mode 100644 index 90a351768..000000000 --- a/backend/app/dto/container.go +++ /dev/null @@ -1,234 +0,0 @@ -package dto - -import ( - "time" -) - -type PageContainer struct { - PageInfo - Name string `json:"name"` - State string `json:"state" validate:"required,oneof=all created running paused restarting removing exited dead"` - OrderBy string `json:"orderBy" validate:"required,oneof=name state created_at"` - Order string `json:"order" validate:"required,oneof=null ascending descending"` - Filters string `json:"filters"` - ExcludeAppStore bool `json:"excludeAppStore"` -} - -type InspectReq struct { - ID string `json:"id" validate:"required"` - Type string `json:"type" validate:"required"` -} - -type ContainerInfo struct { - ContainerID string `json:"containerID"` - Name string `json:"name"` - ImageId string `json:"imageID"` - ImageName string `json:"imageName"` - CreateTime string `json:"createTime"` - State string `json:"state"` - RunTime string `json:"runTime"` - - Network []string `json:"network"` - Ports []string `json:"ports"` - - IsFromApp bool `json:"isFromApp"` - IsFromCompose bool `json:"isFromCompose"` - - AppName string `json:"appName"` - AppInstallName string `json:"appInstallName"` - Websites []string `json:"websites"` -} - -type ResourceLimit struct { - CPU int `json:"cpu"` - Memory uint64 `json:"memory"` -} - -type ContainerOperate struct { - ContainerID string `json:"containerID"` - ForcePull bool `json:"forcePull"` - Name string `json:"name" validate:"required"` - Image string `json:"image" validate:"required"` - Network string `json:"network"` - Ipv4 string `json:"ipv4"` - Ipv6 string `json:"ipv6"` - PublishAllPorts bool `json:"publishAllPorts"` - ExposedPorts []PortHelper `json:"exposedPorts"` - Tty bool `json:"tty"` - OpenStdin bool `json:"openStdin"` - Cmd []string `json:"cmd"` - Entrypoint []string `json:"entrypoint"` - CPUShares int64 `json:"cpuShares"` - NanoCPUs float64 `json:"nanoCPUs"` - Memory float64 `json:"memory"` - Privileged bool `json:"privileged"` - AutoRemove bool `json:"autoRemove"` - Volumes []VolumeHelper `json:"volumes"` - Labels []string `json:"labels"` - Env []string `json:"env"` - RestartPolicy string `json:"restartPolicy"` -} - -type ContainerUpgrade struct { - Name string `json:"name" validate:"required"` - Image string `json:"image" validate:"required"` - ForcePull bool `json:"forcePull"` -} - -type ContainerListStats struct { - ContainerID string `json:"containerID"` - - CPUTotalUsage uint64 `json:"cpuTotalUsage"` - SystemUsage uint64 `json:"systemUsage"` - CPUPercent float64 `json:"cpuPercent"` - PercpuUsage int `json:"percpuUsage"` - - MemoryCache uint64 `json:"memoryCache"` - MemoryUsage uint64 `json:"memoryUsage"` - MemoryLimit uint64 `json:"memoryLimit"` - MemoryPercent float64 `json:"memoryPercent"` -} - -type ContainerStats struct { - CPUPercent float64 `json:"cpuPercent"` - Memory float64 `json:"memory"` - Cache float64 `json:"cache"` - IORead float64 `json:"ioRead"` - IOWrite float64 `json:"ioWrite"` - NetworkRX float64 `json:"networkRX"` - NetworkTX float64 `json:"networkTX"` - - ShotTime time.Time `json:"shotTime"` -} - -type VolumeHelper struct { - Type string `json:"type"` - SourceDir string `json:"sourceDir"` - ContainerDir string `json:"containerDir"` - Mode string `json:"mode"` -} -type PortHelper struct { - HostIP string `json:"hostIP"` - HostPort string `json:"hostPort"` - ContainerPort string `json:"containerPort"` - Protocol string `json:"protocol"` -} - -type ContainerOperation struct { - Names []string `json:"names" validate:"required"` - Operation string `json:"operation" validate:"required,oneof=start stop restart kill pause unpause remove"` -} - -type ContainerRename struct { - Name string `json:"name" validate:"required"` - NewName string `json:"newName" validate:"required"` -} - -type ContainerCommit struct { - ContainerId string `json:"containerID" validate:"required"` - ContainerName string `json:"containerName"` - NewImageName string `json:"newImageName"` - Comment string `json:"comment"` - Author string `json:"author"` - Pause bool `json:"pause"` -} - -type ContainerPrune struct { - PruneType string `json:"pruneType" validate:"required,oneof=container image volume network buildcache"` - WithTagAll bool `json:"withTagAll"` -} - -type ContainerPruneReport struct { - DeletedNumber int `json:"deletedNumber"` - SpaceReclaimed int `json:"spaceReclaimed"` -} - -type Network struct { - ID string `json:"id"` - Name string `json:"name"` - Labels []string `json:"labels"` - Driver string `json:"driver"` - IPAMDriver string `json:"ipamDriver"` - Subnet string `json:"subnet"` - Gateway string `json:"gateway"` - CreatedAt time.Time `json:"createdAt"` - Attachable bool `json:"attachable"` -} -type NetworkCreate struct { - Name string `json:"name" validate:"required"` - Driver string `json:"driver" validate:"required"` - Options []string `json:"options"` - Ipv4 bool `json:"ipv4"` - Subnet string `json:"subnet"` - Gateway string `json:"gateway"` - IPRange string `json:"ipRange"` - AuxAddress []SettingUpdate `json:"auxAddress"` - - Ipv6 bool `json:"ipv6"` - SubnetV6 string `json:"subnetV6"` - GatewayV6 string `json:"gatewayV6"` - IPRangeV6 string `json:"ipRangeV6"` - AuxAddressV6 []SettingUpdate `json:"auxAddressV6"` - Labels []string `json:"labels"` -} - -type Volume struct { - Name string `json:"name"` - Labels []string `json:"labels"` - Driver string `json:"driver"` - Mountpoint string `json:"mountpoint"` - CreatedAt time.Time `json:"createdAt"` -} -type VolumeCreate struct { - Name string `json:"name" validate:"required"` - Driver string `json:"driver" validate:"required"` - Options []string `json:"options"` - Labels []string `json:"labels"` -} - -type BatchDelete struct { - Force bool `json:"force"` - Names []string `json:"names" validate:"required"` -} - -type ComposeInfo struct { - Name string `json:"name"` - CreatedAt string `json:"createdAt"` - CreatedBy string `json:"createdBy"` - ContainerNumber int `json:"containerNumber"` - ConfigFile string `json:"configFile"` - Workdir string `json:"workdir"` - Path string `json:"path"` - Containers []ComposeContainer `json:"containers"` -} -type ComposeContainer struct { - ContainerID string `json:"containerID"` - Name string `json:"name"` - CreateTime string `json:"createTime"` - State string `json:"state"` -} -type ComposeCreate struct { - Name string `json:"name"` - From string `json:"from" validate:"required,oneof=edit path template"` - File string `json:"file"` - Path string `json:"path"` - Template uint `json:"template"` -} -type ComposeOperation struct { - Name string `json:"name" validate:"required"` - Path string `json:"path" validate:"required"` - Operation string `json:"operation" validate:"required,oneof=start stop down"` - WithFile bool `json:"withFile"` -} -type ComposeUpdate struct { - Name string `json:"name" validate:"required"` - Path string `json:"path" validate:"required"` - Content string `json:"content" validate:"required"` -} - -type ContainerLog struct { - Container string `json:"container" validate:"required"` - Since string `json:"since"` - Tail uint `json:"tail"` - ContainerType string `json:"containerType"` -} diff --git a/backend/app/dto/cronjob.go b/backend/app/dto/cronjob.go deleted file mode 100644 index 9189a7e09..000000000 --- a/backend/app/dto/cronjob.go +++ /dev/null @@ -1,121 +0,0 @@ -package dto - -import ( - "time" -) - -type PageCronjob struct { - PageInfo - Info string `json:"info"` - OrderBy string `json:"orderBy" validate:"required,oneof=name status created_at"` - Order string `json:"order" validate:"required,oneof=null ascending descending"` -} - -type CronjobCreate struct { - Name string `json:"name" validate:"required"` - Type string `json:"type" validate:"required"` - Spec string `json:"spec" validate:"required"` - - Script string `json:"script"` - Command string `json:"command"` - ContainerName string `json:"containerName"` - AppID string `json:"appID"` - Website string `json:"website"` - ExclusionRules string `json:"exclusionRules"` - DBType string `json:"dbType"` - DBName string `json:"dbName"` - URL string `json:"url"` - SourceDir string `json:"sourceDir"` - - BackupAccounts string `json:"backupAccounts"` - DefaultDownload string `json:"defaultDownload"` - RetainCopies int `json:"retainCopies" validate:"number,min=1"` - Secret string `json:"secret"` -} - -type CronjobUpdate struct { - ID uint `json:"id" validate:"required"` - Name string `json:"name" validate:"required"` - Spec string `json:"spec" validate:"required"` - - Script string `json:"script"` - Command string `json:"command"` - ContainerName string `json:"containerName"` - AppID string `json:"appID"` - Website string `json:"website"` - ExclusionRules string `json:"exclusionRules"` - DBType string `json:"dbType"` - DBName string `json:"dbName"` - URL string `json:"url"` - SourceDir string `json:"sourceDir"` - - BackupAccounts string `json:"backupAccounts"` - DefaultDownload string `json:"defaultDownload"` - RetainCopies int `json:"retainCopies" validate:"number,min=1"` - Secret string `json:"secret"` -} - -type CronjobUpdateStatus struct { - ID uint `json:"id" validate:"required"` - Status string `json:"status" validate:"required"` -} - -type CronjobDownload struct { - RecordID uint `json:"recordID" validate:"required"` - BackupAccountID uint `json:"backupAccountID" validate:"required"` -} - -type CronjobClean struct { - IsDelete bool `json:"isDelete"` - CleanData bool `json:"cleanData"` - CronjobID uint `json:"cronjobID" validate:"required"` -} - -type CronjobBatchDelete struct { - CleanData bool `json:"cleanData"` - IDs []uint `json:"ids" validate:"required"` -} - -type CronjobInfo struct { - ID uint `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - Spec string `json:"spec"` - - Script string `json:"script"` - Command string `json:"command"` - ContainerName string `json:"containerName"` - AppID string `json:"appID"` - Website string `json:"website"` - ExclusionRules string `json:"exclusionRules"` - DBType string `json:"dbType"` - DBName string `json:"dbName"` - URL string `json:"url"` - SourceDir string `json:"sourceDir"` - BackupAccounts string `json:"backupAccounts"` - DefaultDownload string `json:"defaultDownload"` - RetainCopies int `json:"retainCopies"` - - LastRecordTime string `json:"lastRecordTime"` - Status string `json:"status"` - Secret string `json:"secret"` -} - -type SearchRecord struct { - PageInfo - CronjobID int `json:"cronjobID"` - StartTime time.Time `json:"startTime"` - EndTime time.Time `json:"endTime"` - Status string `json:"status"` -} - -type Record struct { - ID uint `json:"id"` - StartTime string `json:"startTime"` - Records string `json:"records"` - Status string `json:"status"` - Message string `json:"message"` - TargetPath string `json:"targetPath"` - Interval int `json:"interval"` - File string `json:"file"` -} diff --git a/backend/app/dto/dashboard.go b/backend/app/dto/dashboard.go deleted file mode 100644 index a7d3a5c95..000000000 --- a/backend/app/dto/dashboard.go +++ /dev/null @@ -1,107 +0,0 @@ -package dto - -import "time" - -type DashboardBase struct { - WebsiteNumber int `json:"websiteNumber"` - DatabaseNumber int `json:"databaseNumber"` - CronjobNumber int `json:"cronjobNumber"` - AppInstalledNumber int `json:"appInstalledNumber"` - - Hostname string `json:"hostname"` - OS string `json:"os"` - Platform string `json:"platform"` - PlatformFamily string `json:"platformFamily"` - PlatformVersion string `json:"platformVersion"` - KernelArch string `json:"kernelArch"` - KernelVersion string `json:"kernelVersion"` - VirtualizationSystem string `json:"virtualizationSystem"` - - CPUCores int `json:"cpuCores"` - CPULogicalCores int `json:"cpuLogicalCores"` - CPUModelName string `json:"cpuModelName"` - - CurrentInfo DashboardCurrent `json:"currentInfo"` -} - -type OsInfo struct { - OS string `json:"os"` - Platform string `json:"platform"` - PlatformFamily string `json:"platformFamily"` - KernelArch string `json:"kernelArch"` - KernelVersion string `json:"kernelVersion"` - - DiskSize int64 `json:"diskSize"` -} - -type DashboardCurrent struct { - Uptime uint64 `json:"uptime"` - TimeSinceUptime string `json:"timeSinceUptime"` - - Procs uint64 `json:"procs"` - - Load1 float64 `json:"load1"` - Load5 float64 `json:"load5"` - Load15 float64 `json:"load15"` - LoadUsagePercent float64 `json:"loadUsagePercent"` - - CPUPercent []float64 `json:"cpuPercent"` - CPUUsedPercent float64 `json:"cpuUsedPercent"` - CPUUsed float64 `json:"cpuUsed"` - CPUTotal int `json:"cpuTotal"` - - MemoryTotal uint64 `json:"memoryTotal"` - MemoryAvailable uint64 `json:"memoryAvailable"` - MemoryUsed uint64 `json:"memoryUsed"` - MemoryUsedPercent float64 `json:"memoryUsedPercent"` - - SwapMemoryTotal uint64 `json:"swapMemoryTotal"` - SwapMemoryAvailable uint64 `json:"swapMemoryAvailable"` - SwapMemoryUsed uint64 `json:"swapMemoryUsed"` - SwapMemoryUsedPercent float64 `json:"swapMemoryUsedPercent"` - - IOReadBytes uint64 `json:"ioReadBytes"` - IOWriteBytes uint64 `json:"ioWriteBytes"` - IOCount uint64 `json:"ioCount"` - IOReadTime uint64 `json:"ioReadTime"` - IOWriteTime uint64 `json:"ioWriteTime"` - - DiskData []DiskInfo `json:"diskData"` - - NetBytesSent uint64 `json:"netBytesSent"` - NetBytesRecv uint64 `json:"netBytesRecv"` - - GPUData []GPUInfo `json:"gpuData"` - - ShotTime time.Time `json:"shotTime"` -} - -type DiskInfo struct { - Path string `json:"path"` - Type string `json:"type"` - Device string `json:"device"` - Total uint64 `json:"total"` - Free uint64 `json:"free"` - Used uint64 `json:"used"` - UsedPercent float64 `json:"usedPercent"` - - InodesTotal uint64 `json:"inodesTotal"` - InodesUsed uint64 `json:"inodesUsed"` - InodesFree uint64 `json:"inodesFree"` - InodesUsedPercent float64 `json:"inodesUsedPercent"` -} - -type GPUInfo struct { - Index uint `json:"index"` - ProductName string `json:"productName"` - GPUUtil string `json:"gpuUtil"` - Temperature string `json:"temperature"` - PerformanceState string `json:"performanceState"` - PowerUsage string `json:"powerUsage"` - PowerDraw string `json:"powerDraw"` - MaxPowerLimit string `json:"maxPowerLimit"` - MemoryUsage string `json:"memoryUsage"` - MemUsed string `json:"memUsed"` - MemTotal string `json:"memTotal"` - FanSpeed string `json:"fanSpeed"` -} diff --git a/backend/app/dto/database.go b/backend/app/dto/database.go deleted file mode 100644 index acfb3b09e..000000000 --- a/backend/app/dto/database.go +++ /dev/null @@ -1,321 +0,0 @@ -package dto - -import "time" - -// common -type DBConfUpdateByFile struct { - Type string `json:"type" validate:"required,oneof=mysql mariadb postgresql redis"` - Database string `json:"database" validate:"required"` - File string `json:"file"` -} -type ChangeDBInfo struct { - ID uint `json:"id"` - From string `json:"from" validate:"required,oneof=local remote"` - Type string `json:"type" validate:"required,oneof=mysql mariadb postgresql"` - Database string `json:"database" validate:"required"` - Value string `json:"value" validate:"required"` -} - -type DBBaseInfo struct { - Name string `json:"name"` - ContainerName string `json:"containerName"` - Port int64 `json:"port"` -} - -// mysql -type MysqlDBSearch struct { - PageInfo - Info string `json:"info"` - Database string `json:"database" validate:"required"` - OrderBy string `json:"orderBy" validate:"required,oneof=name created_at"` - Order string `json:"order" validate:"required,oneof=null ascending descending"` -} - -type MysqlDBInfo struct { - ID uint `json:"id"` - CreatedAt time.Time `json:"createdAt"` - Name string `json:"name"` - From string `json:"from"` - MysqlName string `json:"mysqlName"` - Format string `json:"format"` - Username string `json:"username"` - Password string `json:"password"` - Permission string `json:"permission"` - IsDelete bool `json:"isDelete"` - Description string `json:"description"` -} - -type MysqlOption struct { - ID uint `json:"id"` - From string `json:"from"` - Type string `json:"type"` - Database string `json:"database"` - Name string `json:"name"` -} - -type MysqlDBCreate struct { - Name string `json:"name" validate:"required"` - From string `json:"from" validate:"required,oneof=local remote"` - Database string `json:"database" validate:"required"` - Format string `json:"format" validate:"required,oneof=utf8mb4 utf8 gbk big5"` - Username string `json:"username" validate:"required"` - Password string `json:"password" validate:"required"` - Permission string `json:"permission" validate:"required"` - Description string `json:"description"` -} - -type BindUser struct { - Database string `json:"database" validate:"required"` - DB string `json:"db" validate:"required"` - Username string `json:"username" validate:"required"` - Password string `json:"password" validate:"required"` - Permission string `json:"permission" validate:"required"` -} - -type MysqlLoadDB struct { - From string `json:"from" validate:"required,oneof=local remote"` - Type string `json:"type" validate:"required,oneof=mysql mariadb"` - Database string `json:"database" validate:"required"` -} - -type MysqlDBDeleteCheck struct { - ID uint `json:"id" validate:"required"` - Type string `json:"type" validate:"required,oneof=mysql mariadb"` - Database string `json:"database" validate:"required"` -} - -type MysqlDBDelete struct { - ID uint `json:"id" validate:"required"` - Type string `json:"type" validate:"required,oneof=mysql mariadb"` - Database string `json:"database" validate:"required"` - ForceDelete bool `json:"forceDelete"` - DeleteBackup bool `json:"deleteBackup"` -} - -type MysqlStatus struct { - AbortedClients string `json:"Aborted_clients"` - AbortedConnects string `json:"Aborted_connects"` - BytesReceived string `json:"Bytes_received"` - BytesSent string `json:"Bytes_sent"` - ComCommit string `json:"Com_commit"` - ComRollback string `json:"Com_rollback"` - Connections string `json:"Connections"` - CreatedTmpDiskTables string `json:"Created_tmp_disk_tables"` - CreatedTmpTables string `json:"Created_tmp_tables"` - InnodbBufferPoolPagesDirty string `json:"Innodb_buffer_pool_pages_dirty"` - InnodbBufferPoolReadRequests string `json:"Innodb_buffer_pool_read_requests"` - InnodbBufferPoolReads string `json:"Innodb_buffer_pool_reads"` - KeyReadRequests string `json:"Key_read_requests"` - KeyReads string `json:"Key_reads"` - KeyWriteEequests string `json:"Key_write_requests"` - KeyWrites string `json:"Key_writes"` - MaxUsedConnections string `json:"Max_used_connections"` - OpenTables string `json:"Open_tables"` - OpenedFiles string `json:"Opened_files"` - OpenedTables string `json:"Opened_tables"` - QcacheHits string `json:"Qcache_hits"` - QcacheInserts string `json:"Qcache_inserts"` - Questions string `json:"Questions"` - SelectFullJoin string `json:"Select_full_join"` - SelectRangeCheck string `json:"Select_range_check"` - SortMergePasses string `json:"Sort_merge_passes"` - TableLocksWaited string `json:"Table_locks_waited"` - ThreadsCached string `json:"Threads_cached"` - ThreadsConnected string `json:"Threads_connected"` - ThreadsCreated string `json:"Threads_created"` - ThreadsRunning string `json:"Threads_running"` - Uptime string `json:"Uptime"` - Run string `json:"Run"` - File string `json:"File"` - Position string `json:"Position"` -} - -type MysqlVariables struct { - BinlogCacheSize string `json:"binlog_cache_size"` - InnodbBufferPoolSize string `json:"innodb_buffer_pool_size"` - InnodbLogBufferSize string `json:"innodb_log_buffer_size"` - JoinBufferSize string `json:"join_buffer_size"` - KeyBufferSize string `json:"key_buffer_size"` - MaxConnections string `json:"max_connections"` - MaxHeapTableSize string `json:"max_heap_table_size"` - QueryCacheSize string `json:"query_cache_size"` - QueryCacheType string `json:"query_cache_type"` - ReadBufferSize string `json:"read_buffer_size"` - ReadRndBufferSize string `json:"read_rnd_buffer_size"` - SortBufferSize string `json:"sort_buffer_size"` - TableOpenCache string `json:"table_open_cache"` - ThreadCacheSize string `json:"thread_cache_size"` - ThreadStack string `json:"thread_stack"` - TmpTableSize string `json:"tmp_table_size"` - - SlowQueryLog string `json:"slow_query_log"` - LongQueryTime string `json:"long_query_time"` -} - -type MysqlVariablesUpdate struct { - Type string `json:"type" validate:"required,oneof=mysql mariadb"` - Database string `json:"database" validate:"required"` - Variables []MysqlVariablesUpdateHelper `json:"variables"` -} - -type MysqlVariablesUpdateHelper struct { - Param string `json:"param"` - Value interface{} `json:"value"` -} - -// redis -type ChangeRedisPass struct { - Database string `json:"database" validate:"required"` - Value string `json:"value"` -} - -type RedisConfUpdate struct { - Database string `json:"database" validate:"required"` - Timeout string `json:"timeout"` - Maxclients string `json:"maxclients"` - Maxmemory string `json:"maxmemory"` -} -type RedisConfPersistenceUpdate struct { - Database string `json:"database" validate:"required"` - Type string `json:"type" validate:"required,oneof=aof rbd"` - Appendonly string `json:"appendonly"` - Appendfsync string `json:"appendfsync"` - Save string `json:"save"` -} - -type RedisConf struct { - Database string `json:"database" validate:"required"` - Name string `json:"name"` - Port int64 `json:"port"` - ContainerName string `json:"containerName"` - Timeout string `json:"timeout"` - Maxclients string `json:"maxclients"` - Requirepass string `json:"requirepass"` - Maxmemory string `json:"maxmemory"` -} - -type RedisPersistence struct { - Database string `json:"database" validate:"required"` - Appendonly string `json:"appendonly"` - Appendfsync string `json:"appendfsync"` - Save string `json:"save"` -} - -type RedisStatus struct { - Database string `json:"database" validate:"required"` - TcpPort string `json:"tcp_port"` - UptimeInDays string `json:"uptime_in_days"` - ConnectedClients string `json:"connected_clients"` - UsedMemory string `json:"used_memory"` - UsedMemoryRss string `json:"used_memory_rss"` - UsedMemoryPeak string `json:"used_memory_peak"` - MemFragmentationRatio string `json:"mem_fragmentation_ratio"` - TotalConnectionsReceived string `json:"total_connections_received"` - TotalCommandsProcessed string `json:"total_commands_processed"` - InstantaneousOpsPerSec string `json:"instantaneous_ops_per_sec"` - KeyspaceHits string `json:"keyspace_hits"` - KeyspaceMisses string `json:"keyspace_misses"` - LatestForkUsec string `json:"latest_fork_usec"` -} - -type DatabaseFileRecords struct { - Database string `json:"database" validate:"required"` - FileName string `json:"fileName"` - FileDir string `json:"fileDir"` - CreatedAt string `json:"createdAt"` - Size int `json:"size"` -} -type RedisBackupRecover struct { - Database string `json:"database" validate:"required"` - FileName string `json:"fileName"` - FileDir string `json:"fileDir"` -} - -// database -type DatabaseSearch struct { - PageInfo - Info string `json:"info"` - Type string `json:"type"` - OrderBy string `json:"orderBy" validate:"required,oneof=name created_at"` - Order string `json:"order" validate:"required,oneof=null ascending descending"` -} - -type DatabaseInfo struct { - ID uint `json:"id"` - CreatedAt time.Time `json:"createdAt"` - Name string `json:"name" validate:"max=256"` - From string `json:"from"` - Type string `json:"type"` - Version string `json:"version"` - Address string `json:"address"` - Port uint `json:"port"` - Username string `json:"username"` - Password string `json:"password"` - - SSL bool `json:"ssl"` - RootCert string `json:"rootCert"` - ClientKey string `json:"clientKey"` - ClientCert string `json:"clientCert"` - SkipVerify bool `json:"skipVerify"` - - Description string `json:"description"` -} - -type DatabaseOption struct { - ID uint `json:"id"` - Type string `json:"type"` - From string `json:"from"` - Database string `json:"database"` - Version string `json:"version"` - Address string `json:"address"` -} - -type DatabaseItem struct { - ID uint `json:"id"` - From string `json:"from"` - Database string `json:"database"` - Name string `json:"name"` -} - -type DatabaseCreate struct { - Name string `json:"name" validate:"required,max=256"` - Type string `json:"type" validate:"required"` - From string `json:"from" validate:"required,oneof=local remote"` - Version string `json:"version" validate:"required"` - Address string `json:"address"` - Port uint `json:"port"` - Username string `json:"username" validate:"required"` - Password string `json:"password"` - - SSL bool `json:"ssl"` - RootCert string `json:"rootCert"` - ClientKey string `json:"clientKey"` - ClientCert string `json:"clientCert"` - SkipVerify bool `json:"skipVerify"` - - Description string `json:"description"` -} - -type DatabaseUpdate struct { - ID uint `json:"id"` - Type string `json:"type" validate:"required"` - Version string `json:"version" validate:"required"` - Address string `json:"address"` - Port uint `json:"port"` - Username string `json:"username" validate:"required"` - Password string `json:"password"` - - SSL bool `json:"ssl"` - RootCert string `json:"rootCert"` - ClientKey string `json:"clientKey"` - ClientCert string `json:"clientCert"` - SkipVerify bool `json:"skipVerify"` - - Description string `json:"description"` -} - -type DatabaseDelete struct { - ID uint `json:"id" validate:"required"` - ForceDelete bool `json:"forceDelete"` - DeleteBackup bool `json:"deleteBackup"` -} diff --git a/backend/app/dto/database_postgresql.go b/backend/app/dto/database_postgresql.go deleted file mode 100644 index 825ff3af5..000000000 --- a/backend/app/dto/database_postgresql.go +++ /dev/null @@ -1,85 +0,0 @@ -package dto - -import "time" - -type PostgresqlDBSearch struct { - PageInfo - Info string `json:"info"` - Database string `json:"database" validate:"required"` - OrderBy string `json:"orderBy" validate:"required,oneof=name created_at"` - Order string `json:"order" validate:"required,oneof=null ascending descending"` -} - -type PostgresqlDBInfo struct { - ID uint `json:"id"` - CreatedAt time.Time `json:"createdAt"` - Name string `json:"name"` - From string `json:"from"` - PostgresqlName string `json:"postgresqlName"` - Format string `json:"format"` - Username string `json:"username"` - Password string `json:"password"` - SuperUser bool `json:"superUser"` - IsDelete bool `json:"isDelete"` - Description string `json:"description"` -} - -type PostgresqlOption struct { - ID uint `json:"id"` - From string `json:"from"` - Type string `json:"type"` - Database string `json:"database"` - Name string `json:"name"` -} - -type PostgresqlDBCreate struct { - Name string `json:"name" validate:"required"` - From string `json:"from" validate:"required,oneof=local remote"` - Database string `json:"database" validate:"required"` - Format string `json:"format"` - Username string `json:"username" validate:"required"` - Password string `json:"password" validate:"required"` - SuperUser bool `json:"superUser"` - Description string `json:"description"` -} - -type PostgresqlBindUser struct { - Name string `json:"name" validate:"required"` - Database string `json:"database" validate:"required"` - Username string `json:"username" validate:"required"` - Password string `json:"password" validate:"required"` - SuperUser bool `json:"superUser"` -} - -type PostgresqlPrivileges struct { - Name string `json:"name" validate:"required"` - Database string `json:"database" validate:"required"` - Username string `json:"username" validate:"required"` - SuperUser bool `json:"superUser"` -} - -type PostgresqlLoadDB struct { - From string `json:"from" validate:"required,oneof=local remote"` - Type string `json:"type" validate:"required,oneof=postgresql"` - Database string `json:"database" validate:"required"` -} - -type PostgresqlDBDeleteCheck struct { - ID uint `json:"id" validate:"required"` - Type string `json:"type" validate:"required,oneof=postgresql"` - Database string `json:"database" validate:"required"` -} - -type PostgresqlDBDelete struct { - ID uint `json:"id" validate:"required"` - Type string `json:"type" validate:"required,oneof=postgresql"` - Database string `json:"database" validate:"required"` - ForceDelete bool `json:"forceDelete"` - DeleteBackup bool `json:"deleteBackup"` -} - -type PostgresqlConfUpdateByFile struct { - Type string `json:"type" validate:"required,oneof=postgresql mariadb"` - Database string `json:"database" validate:"required"` - File string `json:"file"` -} diff --git a/backend/app/dto/device.go b/backend/app/dto/device.go deleted file mode 100644 index ca05cc5cb..000000000 --- a/backend/app/dto/device.go +++ /dev/null @@ -1,41 +0,0 @@ -package dto - -type DeviceBaseInfo struct { - DNS []string `json:"dns"` - Hosts []HostHelper `json:"hosts"` - Hostname string `json:"hostname"` - TimeZone string `json:"timeZone"` - LocalTime string `json:"localTime"` - Ntp string `json:"ntp"` - User string `json:"user"` - - SwapMemoryTotal uint64 `json:"swapMemoryTotal"` - SwapMemoryAvailable uint64 `json:"swapMemoryAvailable"` - SwapMemoryUsed uint64 `json:"swapMemoryUsed"` - MaxSize uint64 `json:"maxSize"` - - SwapDetails []SwapHelper `json:"swapDetails"` -} - -type HostHelper struct { - IP string `json:"ip"` - Host string `json:"host"` -} - -type SwapHelper struct { - Path string `json:"path" validate:"required"` - Size uint64 `json:"size"` - Used string `json:"used"` - - IsNew bool `json:"isNew"` -} - -type TimeZoneOptions struct { - From string `json:"from"` - Zones []string `json:"zones"` -} - -type ChangePasswd struct { - User string `json:"user"` - Passwd string `json:"passwd"` -} diff --git a/backend/app/dto/docker.go b/backend/app/dto/docker.go deleted file mode 100644 index bfabacc41..000000000 --- a/backend/app/dto/docker.go +++ /dev/null @@ -1,39 +0,0 @@ -package dto - -type DaemonJsonUpdateByFile struct { - File string `json:"file"` -} - -type DaemonJsonConf struct { - IsSwarm bool `json:"isSwarm"` - Status string `json:"status"` - Version string `json:"version"` - Mirrors []string `json:"registryMirrors"` - Registries []string `json:"insecureRegistries"` - LiveRestore bool `json:"liveRestore"` - IPTables bool `json:"iptables"` - CgroupDriver string `json:"cgroupDriver"` - - Ipv6 bool `json:"ipv6"` - FixedCidrV6 string `json:"fixedCidrV6"` - Ip6Tables bool `json:"ip6Tables"` - Experimental bool `json:"experimental"` - - LogMaxSize string `json:"logMaxSize"` - LogMaxFile string `json:"logMaxFile"` -} - -type LogOption struct { - LogMaxSize string `json:"logMaxSize"` - LogMaxFile string `json:"logMaxFile"` -} - -type Ipv6Option struct { - FixedCidrV6 string `json:"fixedCidrV6"` - Ip6Tables bool `json:"ip6Tables" validate:"required"` - Experimental bool `json:"experimental"` -} - -type DockerOperation struct { - Operation string `json:"operation" validate:"required,oneof=start restart stop"` -} diff --git a/backend/app/dto/fail2ban.go b/backend/app/dto/fail2ban.go deleted file mode 100644 index 8c8351b82..000000000 --- a/backend/app/dto/fail2ban.go +++ /dev/null @@ -1,29 +0,0 @@ -package dto - -type Fail2BanBaseInfo struct { - IsEnable bool `json:"isEnable"` - IsActive bool `json:"isActive"` - IsExist bool `json:"isExist"` - Version string `json:"version"` - - Port int `json:"port"` - MaxRetry int `json:"maxRetry"` - BanTime string `json:"banTime"` - FindTime string `json:"findTime"` - BanAction string `json:"banAction"` - LogPath string `json:"logPath"` -} - -type Fail2BanSearch struct { - Status string `json:"status" validate:"required,oneof=banned ignore"` -} - -type Fail2BanUpdate struct { - Key string `json:"key" validate:"required,oneof=port bantime findtime maxretry banaction logpath port"` - Value string `json:"value"` -} - -type Fail2BanSet struct { - IPs []string `json:"ips"` - Operate string `json:"operate" validate:"required,oneof=banned ignore"` -} diff --git a/backend/app/dto/firewall.go b/backend/app/dto/firewall.go deleted file mode 100644 index c3d3646e1..000000000 --- a/backend/app/dto/firewall.go +++ /dev/null @@ -1,74 +0,0 @@ -package dto - -type FirewallBaseInfo struct { - Name string `json:"name"` - Status string `json:"status"` - Version string `json:"version"` - PingStatus string `json:"pingStatus"` -} - -type RuleSearch struct { - PageInfo - Info string `json:"info"` - Status string `json:"status"` - Strategy string `json:"strategy"` - Type string `json:"type" validate:"required"` -} - -type FirewallOperation struct { - Operation string `json:"operation" validate:"required,oneof=start stop restart disablePing enablePing"` -} - -type PortRuleOperate struct { - Operation string `json:"operation" validate:"required,oneof=add remove"` - Address string `json:"address"` - Port string `json:"port" validate:"required"` - Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"` - Strategy string `json:"strategy" validate:"required,oneof=accept drop"` - - Description string `json:"description"` -} - -type ForwardRuleOperate struct { - Rules []struct { - Operation string `json:"operation" validate:"required,oneof=add remove"` - Num string `json:"num"` - Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"` - Port string `json:"port" validate:"required"` - TargetIP string `json:"targetIP"` - TargetPort string `json:"targetPort" validate:"required"` - } `json:"rules"` -} - -type UpdateFirewallDescription struct { - Type string `json:"type"` - Address string `json:"address"` - Port string `json:"port"` - Protocol string `json:"protocol"` - Strategy string `json:"strategy" validate:"required,oneof=accept drop"` - - Description string `json:"description"` -} - -type AddrRuleOperate struct { - Operation string `json:"operation" validate:"required,oneof=add remove"` - Address string `json:"address" validate:"required"` - Strategy string `json:"strategy" validate:"required,oneof=accept drop"` - - Description string `json:"description"` -} - -type PortRuleUpdate struct { - OldRule PortRuleOperate `json:"oldRule"` - NewRule PortRuleOperate `json:"newRule"` -} - -type AddrRuleUpdate struct { - OldRule AddrRuleOperate `json:"oldRule"` - NewRule AddrRuleOperate `json:"newRule"` -} - -type BatchRuleOperate struct { - Type string `json:"type" validate:"required"` - Rules []PortRuleOperate `json:"rules"` -} diff --git a/backend/app/dto/ftp.go b/backend/app/dto/ftp.go deleted file mode 100644 index fad9376d0..000000000 --- a/backend/app/dto/ftp.go +++ /dev/null @@ -1,43 +0,0 @@ -package dto - -import ( - "time" -) - -type FtpInfo struct { - ID uint `json:"id"` - CreatedAt time.Time `json:"createdAt"` - - User string `json:"user"` - Password string `json:"password"` - Path string `json:"path"` - Status string `json:"status"` - Description string `json:"description"` -} - -type FtpBaseInfo struct { - IsActive bool `json:"isActive"` - IsExist bool `json:"isExist"` -} - -type FtpLogSearch struct { - PageInfo - User string `json:"user"` - Operation string `json:"operation"` -} - -type FtpCreate struct { - User string `json:"user" validate:"required"` - Password string `json:"password" validate:"required"` - Path string `json:"path" validate:"required"` - Description string `json:"description"` -} - -type FtpUpdate struct { - ID uint `json:"id"` - - Password string `json:"password" validate:"required"` - Path string `json:"path" validate:"required"` - Status string `json:"status"` - Description string `json:"description"` -} diff --git a/backend/app/dto/group.go b/backend/app/dto/group.go deleted file mode 100644 index 60179d7ac..000000000 --- a/backend/app/dto/group.go +++ /dev/null @@ -1,25 +0,0 @@ -package dto - -type GroupCreate struct { - ID uint `json:"id"` - Name string `json:"name" validate:"required"` - Type string `json:"type" validate:"required"` -} - -type GroupSearch struct { - Type string `json:"type" validate:"required"` -} - -type GroupUpdate struct { - ID uint `json:"id"` - Name string `json:"name"` - Type string `json:"type" validate:"required"` - IsDefault bool `json:"isDefault"` -} - -type GroupInfo struct { - ID uint `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - IsDefault bool `json:"isDefault"` -} diff --git a/backend/app/dto/host.go b/backend/app/dto/host.go deleted file mode 100644 index 9853d8f34..000000000 --- a/backend/app/dto/host.go +++ /dev/null @@ -1,75 +0,0 @@ -package dto - -import ( - "time" -) - -type HostOperate struct { - ID uint `json:"id"` - GroupID uint `json:"groupID"` - Name string `json:"name"` - Addr string `json:"addr" validate:"required"` - Port uint `json:"port" validate:"required,number,max=65535,min=1"` - User string `json:"user" validate:"required"` - AuthMode string `json:"authMode" validate:"oneof=password key"` - Password string `json:"password"` - PrivateKey string `json:"privateKey"` - PassPhrase string `json:"passPhrase"` - RememberPassword bool `json:"rememberPassword"` - - Description string `json:"description"` -} - -type HostConnTest struct { - Addr string `json:"addr" validate:"required"` - Port uint `json:"port" validate:"required,number,max=65535,min=1"` - User string `json:"user" validate:"required"` - AuthMode string `json:"authMode" validate:"oneof=password key"` - Password string `json:"password"` - PrivateKey string `json:"privateKey"` - PassPhrase string `json:"passPhrase"` -} - -type SearchHostWithPage struct { - PageInfo - GroupID uint `json:"groupID"` - Info string `json:"info"` -} - -type SearchForTree struct { - Info string `json:"info"` -} - -type ChangeHostGroup struct { - ID uint `json:"id" validate:"required"` - GroupID uint `json:"groupID" validate:"required"` -} - -type HostInfo struct { - ID uint `json:"id"` - CreatedAt time.Time `json:"createdAt"` - GroupID uint `json:"groupID"` - GroupBelong string `json:"groupBelong"` - Name string `json:"name"` - Addr string `json:"addr"` - Port uint `json:"port"` - User string `json:"user"` - AuthMode string `json:"authMode"` - Password string `json:"password"` - PrivateKey string `json:"privateKey"` - PassPhrase string `json:"passPhrase"` - RememberPassword bool `json:"rememberPassword"` - - Description string `json:"description"` -} - -type HostTree struct { - ID uint `json:"id"` - Label string `json:"label"` - Children []TreeChild `json:"children"` -} - -type TreeChild struct { - ID uint `json:"id"` - Label string `json:"label"` -} diff --git a/backend/app/dto/image.go b/backend/app/dto/image.go deleted file mode 100644 index 541c5ffb0..000000000 --- a/backend/app/dto/image.go +++ /dev/null @@ -1,44 +0,0 @@ -package dto - -import "time" - -type ImageInfo struct { - ID string `json:"id"` - CreatedAt time.Time `json:"createdAt"` - IsUsed bool `json:"isUsed"` - Tags []string `json:"tags"` - Size string `json:"size"` -} - -type ImageLoad struct { - Path string `json:"path" validate:"required"` -} - -type ImageBuild struct { - From string `json:"from" validate:"required"` - Name string `json:"name" validate:"required"` - Dockerfile string `json:"dockerfile" validate:"required"` - Tags []string `json:"tags"` -} - -type ImagePull struct { - RepoID uint `json:"repoID"` - ImageName string `json:"imageName" validate:"required"` -} - -type ImageTag struct { - SourceID string `json:"sourceID" validate:"required"` - TargetName string `json:"targetName" validate:"required"` -} - -type ImagePush struct { - RepoID uint `json:"repoID" validate:"required"` - TagName string `json:"tagName" validate:"required"` - Name string `json:"name" validate:"required"` -} - -type ImageSave struct { - TagName string `json:"tagName" validate:"required"` - Path string `json:"path" validate:"required"` - Name string `json:"name" validate:"required"` -} diff --git a/backend/app/dto/image_repo.go b/backend/app/dto/image_repo.go deleted file mode 100644 index ea5275106..000000000 --- a/backend/app/dto/image_repo.go +++ /dev/null @@ -1,44 +0,0 @@ -package dto - -import "time" - -type ImageRepoCreate struct { - Name string `json:"name" validate:"required"` - DownloadUrl string `json:"downloadUrl"` - Protocol string `json:"protocol"` - Username string `json:"username" validate:"max=256"` - Password string `json:"password" validate:"max=256"` - Auth bool `json:"auth"` -} - -type ImageRepoUpdate struct { - ID uint `json:"id"` - DownloadUrl string `json:"downloadUrl"` - Protocol string `json:"protocol"` - Username string `json:"username" validate:"max=256"` - Password string `json:"password" validate:"max=256"` - Auth bool `json:"auth"` -} - -type ImageRepoInfo struct { - ID uint `json:"id"` - CreatedAt time.Time `json:"createdAt"` - Name string `json:"name"` - DownloadUrl string `json:"downloadUrl"` - Protocol string `json:"protocol"` - Username string `json:"username"` - Auth bool `json:"auth"` - - Status string `json:"status"` - Message string `json:"message"` -} - -type ImageRepoOption struct { - ID uint `json:"id"` - Name string `json:"name"` - DownloadUrl string `json:"downloadUrl"` -} - -type ImageRepoDelete struct { - Ids []uint `json:"ids" validate:"required"` -} diff --git a/backend/app/dto/logs.go b/backend/app/dto/logs.go deleted file mode 100644 index 8831801db..000000000 --- a/backend/app/dto/logs.go +++ /dev/null @@ -1,50 +0,0 @@ -package dto - -import ( - "time" -) - -type OperationLog struct { - ID uint `json:"id"` - Source string `json:"source"` - - IP string `json:"ip"` - Path string `json:"path"` - Method string `json:"method"` - UserAgent string `json:"userAgent"` - - Latency time.Duration `json:"latency"` - Status string `json:"status"` - Message string `json:"message"` - - DetailZH string `json:"detailZH"` - DetailEN string `json:"detailEN"` - CreatedAt time.Time `json:"createdAt"` -} - -type SearchOpLogWithPage struct { - PageInfo - Source string `json:"source"` - Status string `json:"status"` - Operation string `json:"operation"` -} - -type SearchLgLogWithPage struct { - PageInfo - IP string `json:"ip"` - Status string `json:"status"` -} - -type LoginLog struct { - ID uint `json:"id"` - IP string `json:"ip"` - Address string `json:"address"` - Agent string `json:"agent"` - Status string `json:"status"` - Message string `json:"message"` - CreatedAt time.Time `json:"createdAt"` -} - -type CleanLog struct { - LogType string `json:"logType" validate:"required,oneof=login operation"` -} diff --git a/backend/app/dto/monitor.go b/backend/app/dto/monitor.go deleted file mode 100644 index 780b1c790..000000000 --- a/backend/app/dto/monitor.go +++ /dev/null @@ -1,16 +0,0 @@ -package dto - -import "time" - -type MonitorSearch struct { - Param string `json:"param" validate:"required,oneof=all cpu memory load io network"` - Info string `json:"info"` - StartTime time.Time `json:"startTime"` - EndTime time.Time `json:"endTime"` -} - -type MonitorData struct { - Param string `json:"param" validate:"required,oneof=cpu memory load io network"` - Date []time.Time `json:"date"` - Value []interface{} `json:"value"` -} diff --git a/backend/app/dto/nginx.go b/backend/app/dto/nginx.go deleted file mode 100644 index 9e9a1f364..000000000 --- a/backend/app/dto/nginx.go +++ /dev/null @@ -1,59 +0,0 @@ -package dto - -import ( - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/utils/nginx/components" -) - -type NginxFull struct { - Install model.AppInstall - Website model.Website - ConfigDir string - ConfigFile string - SiteDir string - Dir string - RootConfig NginxConfig - SiteConfig NginxConfig -} - -type NginxConfig struct { - FilePath string - Config *components.Config - OldContent string -} - -type NginxParam struct { - UpdateScope string - Name string - Params []string -} - -type NginxAuth struct { - Username string `json:"username"` - Remark string `json:"remark"` -} - -type NginxKey string - -const ( - Index NginxKey = "index" - LimitConn NginxKey = "limit-conn" - SSL NginxKey = "ssl" - CACHE NginxKey = "cache" - HttpPer NginxKey = "http-per" - ProxyCache NginxKey = "proxy-cache" -) - -var ScopeKeyMap = map[NginxKey][]string{ - Index: {"index"}, - LimitConn: {"limit_conn", "limit_rate", "limit_conn_zone"}, - SSL: {"ssl_certificate", "ssl_certificate_key"}, - HttpPer: {"server_names_hash_bucket_size", "client_header_buffer_size", "client_max_body_size", "keepalive_timeout", "gzip", "gzip_min_length", "gzip_comp_level"}, -} - -var StaticFileKeyMap = map[NginxKey]struct { -}{ - SSL: {}, - CACHE: {}, - ProxyCache: {}, -} diff --git a/backend/app/dto/request/app.go b/backend/app/dto/request/app.go deleted file mode 100644 index e6e61870d..000000000 --- a/backend/app/dto/request/app.go +++ /dev/null @@ -1,104 +0,0 @@ -package request - -import ( - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" -) - -type AppSearch struct { - dto.PageInfo - Name string `json:"name"` - Tags []string `json:"tags"` - Type string `json:"type"` - Recommend bool `json:"recommend"` - Resource string `json:"resource"` -} - -type AppInstallCreate struct { - AppDetailId uint `json:"appDetailId" validate:"required"` - Params map[string]interface{} `json:"params"` - Name string `json:"name" validate:"required"` - Services map[string]string `json:"services"` - AppContainerConfig -} - -type AppContainerConfig struct { - Advanced bool `json:"advanced"` - CpuQuota float64 `json:"cpuQuota"` - MemoryLimit float64 `json:"memoryLimit"` - MemoryUnit string `json:"memoryUnit"` - ContainerName string `json:"containerName"` - AllowPort bool `json:"allowPort"` - EditCompose bool `json:"editCompose"` - DockerCompose string `json:"dockerCompose"` - HostMode bool `json:"hostMode"` - PullImage bool `json:"pullImage"` -} - -type AppInstalledSearch struct { - dto.PageInfo - Type string `json:"type"` - Name string `json:"name"` - Tags []string `json:"tags"` - Update bool `json:"update"` - Unused bool `json:"unused"` - All bool `json:"all"` - Sync bool `json:"sync"` -} - -type AppInstalledInfo struct { - Key string `json:"key" validate:"required"` - Name string `json:"name"` -} - -type AppBackupSearch struct { - dto.PageInfo - AppInstallID uint `json:"appInstallID"` -} - -type AppBackupDelete struct { - Ids []uint `json:"ids"` -} - -type AppInstalledOperate struct { - InstallId uint `json:"installId" validate:"required"` - BackupId uint `json:"backupId"` - DetailId uint `json:"detailId"` - Operate constant.AppOperate `json:"operate" validate:"required"` - ForceDelete bool `json:"forceDelete"` - DeleteBackup bool `json:"deleteBackup"` - DeleteDB bool `json:"deleteDB"` - Backup bool `json:"backup"` - PullImage bool `json:"pullImage"` - DockerCompose string `json:"dockerCompose"` -} - -type AppInstallUpgrade struct { - InstallID uint `json:"installId"` - DetailID uint `json:"detailId"` - Backup bool `json:"backup"` - PullImage bool `json:"pullImage"` - DockerCompose string `json:"dockerCompose"` -} - -type AppInstalledUpdate struct { - InstallId uint `json:"installId" validate:"required"` - Params map[string]interface{} `json:"params" validate:"required"` - AppContainerConfig -} - -type AppInstalledIgnoreUpgrade struct { - DetailID uint `json:"detailID" validate:"required"` - Operate string `json:"operate" validate:"required,oneof=cancel ignore"` -} - -type PortUpdate struct { - Key string `json:"key"` - Name string `json:"name"` - Port int64 `json:"port"` -} - -type AppUpdateVersion struct { - AppInstallID uint `json:"appInstallID" validate:"required"` - UpdateVersion string `json:"updateVersion"` -} diff --git a/backend/app/dto/request/favorite.go b/backend/app/dto/request/favorite.go deleted file mode 100644 index 70280929c..000000000 --- a/backend/app/dto/request/favorite.go +++ /dev/null @@ -1,9 +0,0 @@ -package request - -type FavoriteCreate struct { - Path string `json:"path" validate:"required"` -} - -type FavoriteDelete struct { - ID uint `json:"id" validate:"required"` -} diff --git a/backend/app/dto/request/file.go b/backend/app/dto/request/file.go deleted file mode 100644 index 96c771dab..000000000 --- a/backend/app/dto/request/file.go +++ /dev/null @@ -1,136 +0,0 @@ -package request - -import ( - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/utils/files" -) - -type FileOption struct { - files.FileOption -} - -type FileContentReq struct { - Path string `json:"path" validate:"required"` - IsDetail bool `json:"isDetail"` -} - -type SearchUploadWithPage struct { - dto.PageInfo - Path string `json:"path" validate:"required"` -} - -type FileCreate struct { - Path string `json:"path" validate:"required"` - Content string `json:"content"` - IsDir bool `json:"isDir"` - Mode int64 `json:"mode"` - IsLink bool `json:"isLink"` - IsSymlink bool `json:"isSymlink"` - LinkPath string `json:"linkPath"` - Sub bool `json:"sub"` -} - -type FileRoleReq struct { - Paths []string `json:"paths" validate:"required"` - Mode int64 `json:"mode" validate:"required"` - User string `json:"user" validate:"required"` - Group string `json:"group" validate:"required"` - Sub bool `json:"sub"` -} - -type FileDelete struct { - Path string `json:"path" validate:"required"` - IsDir bool `json:"isDir"` - ForceDelete bool `json:"forceDelete"` -} - -type FileBatchDelete struct { - Paths []string `json:"paths" validate:"required"` - IsDir bool `json:"isDir"` -} - -type FileCompress struct { - Files []string `json:"files" validate:"required"` - Dst string `json:"dst" validate:"required"` - Type string `json:"type" validate:"required"` - Name string `json:"name" validate:"required"` - Replace bool `json:"replace"` - Secret string `json:"secret"` -} - -type FileDeCompress struct { - Dst string `json:"dst" validate:"required"` - Type string `json:"type" validate:"required"` - Path string `json:"path" validate:"required"` - Secret string `json:"secret"` -} - -type FileEdit struct { - Path string `json:"path" validate:"required"` - Content string `json:"content"` -} - -type FileRename struct { - OldName string `json:"oldName" validate:"required"` - NewName string `json:"newName" validate:"required"` -} - -type FilePathCheck struct { - Path string `json:"path" validate:"required"` -} - -type FileWget struct { - Url string `json:"url" validate:"required"` - Path string `json:"path" validate:"required"` - Name string `json:"name" validate:"required"` - IgnoreCertificate bool `json:"ignoreCertificate"` -} - -type FileMove struct { - Type string `json:"type" validate:"required"` - OldPaths []string `json:"oldPaths" validate:"required"` - NewPath string `json:"newPath" validate:"required"` - Name string `json:"name"` - Cover bool `json:"cover"` -} - -type FileDownload struct { - Paths []string `json:"paths" validate:"required"` - Type string `json:"type" validate:"required"` - Name string `json:"name" validate:"required"` - Compress bool `json:"compress"` -} - -type FileChunkDownload struct { - Path string `json:"path" validate:"required"` - Name string `json:"name" validate:"required"` -} - -type DirSizeReq struct { - Path string `json:"path" validate:"required"` -} - -type FileProcessReq struct { - Key string `json:"key"` -} - -type FileRoleUpdate struct { - Path string `json:"path" validate:"required"` - User string `json:"user" validate:"required"` - Group string `json:"group" validate:"required"` - Sub bool `json:"sub"` -} - -type FileReadByLineReq struct { - Page int `json:"page" validate:"required"` - PageSize int `json:"pageSize" validate:"required"` - Type string `json:"type" validate:"required"` - ID uint `json:"ID"` - Name string `json:"name"` - Latest bool `json:"latest"` -} - -type FileExistReq struct { - Name string `json:"name" validate:"required"` - Dir string `json:"dir" validate:"required"` -} diff --git a/backend/app/dto/request/host_tool.go b/backend/app/dto/request/host_tool.go deleted file mode 100644 index aef43182d..000000000 --- a/backend/app/dto/request/host_tool.go +++ /dev/null @@ -1,41 +0,0 @@ -package request - -type HostToolReq struct { - Type string `json:"type" validate:"required,oneof=supervisord"` - Operate string `json:"operate" validate:"oneof=status restart start stop"` -} - -type HostToolCreate struct { - Type string `json:"type" validate:"required"` - SupervisorConfig -} - -type SupervisorConfig struct { - ConfigPath string `json:"configPath"` - ServiceName string `json:"serviceName"` -} - -type HostToolLogReq struct { - Type string `json:"type" validate:"required,oneof=supervisord"` -} - -type HostToolConfig struct { - Type string `json:"type" validate:"required,oneof=supervisord"` - Operate string `json:"operate" validate:"oneof=get set"` - Content string `json:"content"` -} - -type SupervisorProcessConfig struct { - Name string `json:"name"` - Operate string `json:"operate"` - Command string `json:"command"` - User string `json:"user"` - Dir string `json:"dir"` - Numprocs string `json:"numprocs"` -} -type SupervisorProcessFileReq struct { - Name string `json:"name" validate:"required"` - Operate string `json:"operate" validate:"required,oneof=get clear update" ` - Content string `json:"content"` - File string `json:"file" validate:"required,oneof=out.log err.log config"` -} diff --git a/backend/app/dto/request/nginx.go b/backend/app/dto/request/nginx.go deleted file mode 100644 index fd2b68111..000000000 --- a/backend/app/dto/request/nginx.go +++ /dev/null @@ -1,87 +0,0 @@ -package request - -import "github.com/1Panel-dev/1Panel/backend/app/dto" - -type NginxConfigFileUpdate struct { - Content string `json:"content" validate:"required"` - Backup bool `json:"backup"` -} - -type NginxScopeReq struct { - Scope dto.NginxKey `json:"scope" validate:"required"` - WebsiteID uint `json:"websiteId"` -} - -type NginxConfigUpdate struct { - Scope dto.NginxKey `json:"scope"` - Operate string `json:"operate" validate:"required,oneof=add update delete"` - WebsiteID uint `json:"websiteId"` - Params interface{} `json:"params"` -} - -type NginxRewriteReq struct { - WebsiteID uint `json:"websiteId" validate:"required"` - Name string `json:"name" validate:"required"` -} - -type NginxRewriteUpdate struct { - WebsiteID uint `json:"websiteId" validate:"required"` - Name string `json:"name" validate:"required"` - Content string `json:"content"` -} - -type NginxProxyUpdate struct { - WebsiteID uint `json:"websiteID" validate:"required"` - Content string `json:"content" validate:"required"` - Name string `json:"name" validate:"required"` -} - -type NginxAuthUpdate struct { - WebsiteID uint `json:"websiteID" validate:"required"` - Operate string `json:"operate" validate:"required"` - Username string `json:"username"` - Password string `json:"password"` - Remark string `json:"remark"` -} - -type NginxAuthReq struct { - WebsiteID uint `json:"websiteID" validate:"required"` -} - -type NginxCommonReq struct { - WebsiteID uint `json:"websiteID" validate:"required"` -} - -type NginxAntiLeechUpdate struct { - WebsiteID uint `json:"websiteID" validate:"required"` - Extends string `json:"extends" validate:"required"` - Return string `json:"return" validate:"required"` - Enable bool `json:"enable" ` - ServerNames []string `json:"serverNames"` - Cache bool `json:"cache"` - CacheTime int `json:"cacheTime"` - CacheUint string `json:"cacheUint"` - NoneRef bool `json:"noneRef"` - LogEnable bool `json:"logEnable"` - Blocked bool `json:"blocked"` -} - -type NginxRedirectReq struct { - Name string `json:"name" validate:"required"` - WebsiteID uint `json:"websiteID" validate:"required"` - Domains []string `json:"domains"` - KeepPath bool `json:"keepPath"` - Enable bool `json:"enable"` - Type string `json:"type" validate:"required"` - Redirect string `json:"redirect" validate:"required"` - Path string `json:"path"` - Target string `json:"target" validate:"required"` - Operate string `json:"operate" validate:"required"` - RedirectRoot bool `json:"redirectRoot"` -} - -type NginxRedirectUpdate struct { - WebsiteID uint `json:"websiteID" validate:"required"` - Content string `json:"content" validate:"required"` - Name string `json:"name" validate:"required"` -} diff --git a/backend/app/dto/request/php_extensions.go b/backend/app/dto/request/php_extensions.go deleted file mode 100644 index a163859ff..000000000 --- a/backend/app/dto/request/php_extensions.go +++ /dev/null @@ -1,22 +0,0 @@ -package request - -import "github.com/1Panel-dev/1Panel/backend/app/dto" - -type PHPExtensionsSearch struct { - dto.PageInfo - All bool `json:"all"` -} - -type PHPExtensionsCreate struct { - Name string `json:"name" validate:"required"` - Extensions string `json:"extensions" validate:"required"` -} - -type PHPExtensionsUpdate struct { - ID uint `json:"id" validate:"required"` - Extensions string `json:"extensions" validate:"required"` -} - -type PHPExtensionsDelete struct { - ID uint `json:"id" validate:"required"` -} diff --git a/backend/app/dto/request/process.go b/backend/app/dto/request/process.go deleted file mode 100644 index 9269c4c7d..000000000 --- a/backend/app/dto/request/process.go +++ /dev/null @@ -1,5 +0,0 @@ -package request - -type ProcessReq struct { - PID int32 `json:"PID" validate:"required"` -} diff --git a/backend/app/dto/request/recycle_bin.go b/backend/app/dto/request/recycle_bin.go deleted file mode 100644 index 4870a735e..000000000 --- a/backend/app/dto/request/recycle_bin.go +++ /dev/null @@ -1,11 +0,0 @@ -package request - -type RecycleBinCreate struct { - SourcePath string `json:"sourcePath" validate:"required"` -} - -type RecycleBinReduce struct { - From string `json:"from" validate:"required"` - RName string `json:"rName" validate:"required"` - Name string `json:"name"` -} diff --git a/backend/app/dto/request/runtime.go b/backend/app/dto/request/runtime.go deleted file mode 100644 index d9c218500..000000000 --- a/backend/app/dto/request/runtime.go +++ /dev/null @@ -1,72 +0,0 @@ -package request - -import "github.com/1Panel-dev/1Panel/backend/app/dto" - -type RuntimeSearch struct { - dto.PageInfo - Type string `json:"type"` - Name string `json:"name"` - Status string `json:"status"` -} - -type RuntimeCreate struct { - AppDetailID uint `json:"appDetailId"` - Name string `json:"name"` - Params map[string]interface{} `json:"params"` - Resource string `json:"resource"` - Image string `json:"image"` - Type string `json:"type"` - Version string `json:"version"` - Source string `json:"source"` - CodeDir string `json:"codeDir"` - NodeConfig -} - -type NodeConfig struct { - Install bool `json:"install"` - Clean bool `json:"clean"` - Port int `json:"port"` - ExposedPorts []ExposedPort `json:"exposedPorts"` -} - -type ExposedPort struct { - HostPort int `json:"hostPort"` - ContainerPort int `json:"containerPort"` -} - -type RuntimeDelete struct { - ID uint `json:"id"` - ForceDelete bool `json:"forceDelete"` -} - -type RuntimeUpdate struct { - Name string `json:"name"` - ID uint `json:"id"` - Params map[string]interface{} `json:"params"` - Image string `json:"image"` - Version string `json:"version"` - Rebuild bool `json:"rebuild"` - Source string `json:"source"` - CodeDir string `json:"codeDir"` - NodeConfig -} - -type NodePackageReq struct { - CodeDir string `json:"codeDir"` -} - -type RuntimeOperate struct { - Operate string `json:"operate"` - ID uint `json:"ID"` -} - -type NodeModuleOperateReq struct { - Operate string `json:"operate" validate:"oneof=install uninstall update"` - ID uint `json:"ID" validate:"required"` - Module string `json:"module"` - PkgManager string `json:"pkgManager" validate:"oneof=npm yarn"` -} - -type NodeModuleReq struct { - ID uint `json:"ID" validate:"required"` -} diff --git a/backend/app/dto/request/website.go b/backend/app/dto/request/website.go deleted file mode 100644 index aac5baca1..000000000 --- a/backend/app/dto/request/website.go +++ /dev/null @@ -1,220 +0,0 @@ -package request - -import ( - "github.com/1Panel-dev/1Panel/backend/app/dto" -) - -type WebsiteSearch struct { - dto.PageInfo - Name string `json:"name"` - OrderBy string `json:"orderBy" validate:"required,oneof=primary_domain type status created_at expire_date"` - Order string `json:"order" validate:"required,oneof=null ascending descending"` - WebsiteGroupID uint `json:"websiteGroupId"` -} - -type WebsiteCreate struct { - PrimaryDomain string `json:"primaryDomain" validate:"required"` - Type string `json:"type" validate:"required"` - Alias string `json:"alias" validate:"required"` - Remark string `json:"remark"` - OtherDomains string `json:"otherDomains"` - Proxy string `json:"proxy"` - WebsiteGroupID uint `json:"webSiteGroupID" validate:"required"` - IPV6 bool `json:"IPV6"` - - AppType string `json:"appType" validate:"oneof=new installed"` - AppInstall NewAppInstall `json:"appInstall"` - AppID uint `json:"appID"` - AppInstallID uint `json:"appInstallID"` - - FtpUser string `json:"ftpUser"` - FtpPassword string `json:"ftpPassword"` - - RuntimeID uint `json:"runtimeID"` - RuntimeConfig -} - -type RuntimeConfig struct { - ProxyType string `json:"proxyType"` - Port int `json:"port"` -} - -type NewAppInstall struct { - Name string `json:"name"` - AppDetailId uint `json:"appDetailID"` - Params map[string]interface{} `json:"params"` - - AppContainerConfig -} - -type WebsiteInstallCheckReq struct { - InstallIds []uint `json:"InstallIds"` -} - -type WebsiteUpdate struct { - ID uint `json:"id" validate:"required"` - PrimaryDomain string `json:"primaryDomain" validate:"required"` - Remark string `json:"remark"` - WebsiteGroupID uint `json:"webSiteGroupID"` - ExpireDate string `json:"expireDate"` - IPV6 bool `json:"IPV6"` -} - -type WebsiteDelete struct { - ID uint `json:"id" validate:"required"` - DeleteApp bool `json:"deleteApp"` - DeleteBackup bool `json:"deleteBackup"` - ForceDelete bool `json:"forceDelete"` -} - -type WebsiteOp struct { - ID uint `json:"id" validate:"required"` - Operate string `json:"operate"` -} - -type WebsiteRedirectUpdate struct { - WebsiteID uint `json:"websiteId" validate:"required"` - Key string `json:"key" validate:"required"` - Enable bool `json:"enable"` -} - -type WebsiteRecover struct { - WebsiteName string `json:"websiteName" validate:"required"` - Type string `json:"type" validate:"required"` - BackupName string `json:"backupName" validate:"required"` -} - -type WebsiteRecoverByFile struct { - WebsiteName string `json:"websiteName" validate:"required"` - Type string `json:"type" validate:"required"` - FileDir string `json:"fileDir" validate:"required"` - FileName string `json:"fileName" validate:"required"` -} - -type WebsiteGroupCreate struct { - Name string `json:"name" validate:"required"` -} - -type WebsiteGroupUpdate struct { - ID uint `json:"id" validate:"required"` - Name string `json:"name"` - Default bool `json:"default"` -} - -type WebsiteDomainCreate struct { - WebsiteID uint `json:"websiteID" validate:"required"` - Domains string `json:"domains" validate:"required"` -} - -type WebsiteDomainDelete struct { - ID uint `json:"id" validate:"required"` -} - -type WebsiteHTTPSOp struct { - WebsiteID uint `json:"websiteId" validate:"required"` - Enable bool `json:"enable"` - WebsiteSSLID uint `json:"websiteSSLId"` - Type string `json:"type" validate:"oneof=existed auto manual"` - PrivateKey string `json:"privateKey"` - Certificate string `json:"certificate"` - PrivateKeyPath string `json:"privateKeyPath"` - CertificatePath string `json:"certificatePath"` - ImportType string `json:"importType"` - HttpConfig string `json:"httpConfig" validate:"oneof=HTTPSOnly HTTPAlso HTTPToHTTPS"` - SSLProtocol []string `json:"SSLProtocol"` - Algorithm string `json:"algorithm"` - Hsts bool `json:"hsts"` -} - -type WebsiteNginxUpdate struct { - ID uint `json:"id" validate:"required"` - Content string `json:"content" validate:"required"` -} - -type WebsiteLogReq struct { - ID uint `json:"id" validate:"required"` - Operate string `json:"operate" validate:"required"` - LogType string `json:"logType" validate:"required"` - Page int `json:"page"` - PageSize int `json:"pageSize"` -} - -type WebsiteDefaultUpdate struct { - ID uint `json:"id"` -} - -type WebsitePHPConfigUpdate struct { - ID uint `json:"id" validate:"required"` - Params map[string]string `json:"params"` - Scope string `json:"scope" validate:"required"` - DisableFunctions []string `json:"disableFunctions"` - UploadMaxSize string `json:"uploadMaxSize"` -} - -type WebsitePHPFileUpdate struct { - ID uint `json:"id" validate:"required"` - Type string `json:"type" validate:"required"` - Content string `json:"content" validate:"required"` -} - -type WebsitePHPVersionReq struct { - WebsiteID uint `json:"websiteID" validate:"required"` - RuntimeID uint `json:"runtimeID" validate:"required"` - RetainConfig bool `json:"retainConfig" ` -} - -type WebsiteUpdateDir struct { - ID uint `json:"id" validate:"required"` - SiteDir string `json:"siteDir" validate:"required"` -} - -type WebsiteUpdateDirPermission struct { - ID uint `json:"id" validate:"required"` - User string `json:"user" validate:"required"` - Group string `json:"group" validate:"required"` -} - -type WebsiteProxyConfig struct { - ID uint `json:"id" validate:"required"` - Operate string `json:"operate" validate:"required"` - Enable bool `json:"enable" ` - Cache bool `json:"cache" ` - CacheTime int `json:"cacheTime" ` - CacheUnit string `json:"cacheUnit"` - Name string `json:"name" validate:"required"` - Modifier string `json:"modifier"` - Match string `json:"match" validate:"required"` - ProxyPass string `json:"proxyPass" validate:"required"` - ProxyHost string `json:"proxyHost" validate:"required"` - Content string `json:"content"` - FilePath string `json:"filePath"` - Replaces map[string]string `json:"replaces"` - SNI bool `json:"sni"` -} - -type WebsiteProxyReq struct { - ID uint `json:"id" validate:"required"` -} - -type WebsiteRedirectReq struct { - WebsiteID uint `json:"websiteId" validate:"required"` -} - -type WebsiteCommonReq struct { - ID uint `json:"id" validate:"required"` -} - -type WafWebsite struct { - Key string `json:"key"` - Domains []string `json:"domains"` - Host []string `json:"host"` -} - -type WebsiteHtmlReq struct { - Type string `json:"type" validate:"required"` -} - -type WebsiteHtmlUpdate struct { - Type string `json:"type" validate:"required"` - Content string `json:"content" validate:"required"` -} diff --git a/backend/app/dto/request/website_ssl.go b/backend/app/dto/request/website_ssl.go deleted file mode 100644 index 509aed434..000000000 --- a/backend/app/dto/request/website_ssl.go +++ /dev/null @@ -1,139 +0,0 @@ -package request - -import "github.com/1Panel-dev/1Panel/backend/app/dto" - -type WebsiteSSLSearch struct { - dto.PageInfo - AcmeAccountID string `json:"acmeAccountID"` -} - -type WebsiteSSLCreate struct { - PrimaryDomain string `json:"primaryDomain" validate:"required"` - OtherDomains string `json:"otherDomains"` - Provider string `json:"provider" validate:"required"` - AcmeAccountID uint `json:"acmeAccountId" validate:"required"` - DnsAccountID uint `json:"dnsAccountId"` - AutoRenew bool `json:"autoRenew"` - KeyType string `json:"keyType"` - Apply bool `json:"apply"` - PushDir bool `json:"pushDir"` - Dir string `json:"dir"` - ID uint `json:"id"` - Description string `json:"description"` - DisableCNAME bool `json:"disableCNAME"` - SkipDNS bool `json:"skipDNS"` - Nameserver1 string `json:"nameserver1"` - Nameserver2 string `json:"nameserver2"` - ExecShell bool `json:"execShell"` - Shell string `json:"shell"` -} - -type WebsiteDNSReq struct { - Domains []string `json:"domains" validate:"required"` - AcmeAccountID uint `json:"acmeAccountId" validate:"required"` -} - -type WebsiteSSLRenew struct { - SSLID uint `json:"SSLId" validate:"required"` -} - -type WebsiteSSLApply struct { - ID uint `json:"ID" validate:"required"` - SkipDNSCheck bool `json:"skipDNSCheck"` - Nameservers []string `json:"nameservers"` -} - -type WebsiteAcmeAccountCreate struct { - Email string `json:"email" validate:"required"` - Type string `json:"type" validate:"required,oneof=letsencrypt zerossl buypass google"` - KeyType string `json:"keyType" validate:"required,oneof=P256 P384 2048 3072 4096 8192"` - EabKid string `json:"eabKid"` - EabHmacKey string `json:"eabHmacKey"` -} - -type WebsiteDnsAccountCreate struct { - Name string `json:"name" validate:"required"` - Type string `json:"type" validate:"required"` - Authorization map[string]string `json:"authorization" validate:"required"` -} - -type WebsiteDnsAccountUpdate struct { - ID uint `json:"id" validate:"required"` - Name string `json:"name" validate:"required"` - Type string `json:"type" validate:"required"` - Authorization map[string]string `json:"authorization" validate:"required"` -} - -type WebsiteResourceReq struct { - ID uint `json:"id" validate:"required"` -} - -type WebsiteBatchDelReq struct { - IDs []uint `json:"ids" validate:"required"` -} - -type WebsiteSSLUpdate struct { - ID uint `json:"id" validate:"required"` - AutoRenew bool `json:"autoRenew"` - Description string `json:"description"` - PrimaryDomain string `json:"primaryDomain" validate:"required"` - OtherDomains string `json:"otherDomains"` - Provider string `json:"provider" validate:"required"` - AcmeAccountID uint `json:"acmeAccountId"` - DnsAccountID uint `json:"dnsAccountId"` - KeyType string `json:"keyType"` - Apply bool `json:"apply"` - PushDir bool `json:"pushDir"` - Dir string `json:"dir"` - DisableCNAME bool `json:"disableCNAME"` - SkipDNS bool `json:"skipDNS"` - Nameserver1 string `json:"nameserver1"` - Nameserver2 string `json:"nameserver2"` - ExecShell bool `json:"execShell"` - Shell string `json:"shell"` -} - -type WebsiteSSLUpload struct { - PrivateKey string `json:"privateKey"` - Certificate string `json:"certificate"` - PrivateKeyPath string `json:"privateKeyPath"` - CertificatePath string `json:"certificatePath"` - Type string `json:"type" validate:"required,oneof=paste local"` - SSLID uint `json:"sslID"` - Description string `json:"description"` -} - -type WebsiteCASearch struct { - dto.PageInfo -} - -type WebsiteCACreate struct { - CommonName string `json:"commonName" validate:"required"` - Country string `json:"country" validate:"required"` - Organization string `json:"organization" validate:"required"` - OrganizationUint string `json:"organizationUint"` - Name string `json:"name" validate:"required"` - KeyType string `json:"keyType" validate:"required,oneof=P256 P384 2048 3072 4096 8192"` - Province string `json:"province" ` - City string `json:"city"` -} - -type WebsiteCAObtain struct { - ID uint `json:"id" validate:"required"` - Domains string `json:"domains" validate:"required"` - KeyType string `json:"keyType" validate:"required,oneof=P256 P384 2048 3072 4096 8192"` - Time int `json:"time" validate:"required"` - Unit string `json:"unit" validate:"required"` - PushDir bool `json:"pushDir"` - Dir string `json:"dir"` - AutoRenew bool `json:"autoRenew"` - Renew bool `json:"renew"` - SSLID uint `json:"sslID"` - Description string `json:"description"` - ExecShell bool `json:"execShell"` - Shell string `json:"shell"` -} - -type WebsiteCARenew struct { - SSLID uint `json:"SSLID" validate:"required"` -} diff --git a/backend/app/dto/response/app.go b/backend/app/dto/response/app.go deleted file mode 100644 index 2cbced1ef..000000000 --- a/backend/app/dto/response/app.go +++ /dev/null @@ -1,154 +0,0 @@ -package response - -import ( - "time" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - - "github.com/1Panel-dev/1Panel/backend/app/model" -) - -type AppRes struct { - Items []*AppDto `json:"items"` - Total int64 `json:"total"` -} - -type AppUpdateRes struct { - CanUpdate bool `json:"canUpdate"` - IsSyncing bool `json:"isSyncing"` - AppStoreLastModified int `json:"appStoreLastModified"` - AppList *dto.AppList `json:"appList"` -} - -type AppDTO struct { - model.App - Installed bool `json:"installed"` - Versions []string `json:"versions"` - Tags []model.Tag `json:"tags"` -} - -type AppDto struct { - Name string `json:"name"` - Key string `json:"key"` - ID uint `json:"id"` - ShortDescZh string `json:"shortDescZh"` - ShortDescEn string `json:"shortDescEn"` - Icon string `json:"icon"` - Type string `json:"type"` - Status string `json:"status"` - Resource string `json:"resource"` - Installed bool `json:"installed"` - Versions []string `json:"versions"` - Limit int `json:"limit"` - Tags []model.Tag `json:"tags"` -} - -type TagDTO struct { - model.Tag -} - -type AppInstalledCheck struct { - IsExist bool `json:"isExist"` - Name string `json:"name"` - App string `json:"app"` - Version string `json:"version"` - Status string `json:"status"` - CreatedAt time.Time `json:"createdAt"` - LastBackupAt string `json:"lastBackupAt"` - AppInstallID uint `json:"appInstallId"` - ContainerName string `json:"containerName"` - InstallPath string `json:"installPath"` - HttpPort int `json:"httpPort"` - HttpsPort int `json:"httpsPort"` -} - -type AppDetailDTO struct { - model.AppDetail - Enable bool `json:"enable"` - Params interface{} `json:"params"` - Image string `json:"image"` - HostMode bool `json:"hostMode"` -} - -type IgnoredApp struct { - Icon string `json:"icon"` - Name string `json:"name"` - Version string `json:"version"` - DetailID uint `json:"detailID"` -} - -type AppInstalledDTO struct { - model.AppInstall - Total int `json:"total"` - Ready int `json:"ready"` - AppName string `json:"appName"` - Icon string `json:"icon"` - CanUpdate bool `json:"canUpdate"` - Path string `json:"path"` -} - -type AppDetail struct { - Website string `json:"website"` - Document string `json:"document"` - Github string `json:"github"` -} - -type AppInstallDTO struct { - ID uint `json:"id"` - Name string `json:"name"` - AppID uint `json:"appID"` - AppDetailID uint `json:"appDetailID"` - Version string `json:"version"` - Status string `json:"status"` - Message string `json:"message"` - HttpPort int `json:"httpPort"` - HttpsPort int `json:"httpsPort"` - Path string `json:"path"` - CanUpdate bool `json:"canUpdate"` - Icon string `json:"icon"` - AppName string `json:"appName"` - Ready int `json:"ready"` - Total int `json:"total"` - AppKey string `json:"appKey"` - AppType string `json:"appType"` - AppStatus string `json:"appStatus"` - DockerCompose string `json:"dockerCompose"` - CreatedAt time.Time `json:"createdAt"` - App AppDetail `json:"app"` -} - -type DatabaseConn struct { - Status string `json:"status"` - Username string `json:"username"` - Password string `json:"password"` - ContainerName string `json:"containerName"` - ServiceName string `json:"serviceName"` - Port int64 `json:"port"` -} - -type AppService struct { - Label string `json:"label"` - Value string `json:"value"` - Config interface{} `json:"config"` - From string `json:"from"` -} - -type AppParam struct { - Value interface{} `json:"value"` - Edit bool `json:"edit"` - Key string `json:"key"` - Rule string `json:"rule"` - LabelZh string `json:"labelZh"` - LabelEn string `json:"labelEn"` - Type string `json:"type"` - Values interface{} `json:"values"` - ShowValue string `json:"showValue"` - Required bool `json:"required"` - Multiple bool `json:"multiple"` -} - -type AppConfig struct { - Params []AppParam `json:"params"` - request.AppContainerConfig -} diff --git a/backend/app/dto/response/favorite.go b/backend/app/dto/response/favorite.go deleted file mode 100644 index b00a6c475..000000000 --- a/backend/app/dto/response/favorite.go +++ /dev/null @@ -1,7 +0,0 @@ -package response - -import "github.com/1Panel-dev/1Panel/backend/app/model" - -type FavoriteDTO struct { - model.Favorite -} diff --git a/backend/app/dto/response/file.go b/backend/app/dto/response/file.go deleted file mode 100644 index 288260800..000000000 --- a/backend/app/dto/response/file.go +++ /dev/null @@ -1,47 +0,0 @@ -package response - -import ( - "github.com/1Panel-dev/1Panel/backend/utils/files" -) - -type FileInfo struct { - files.FileInfo -} - -type UploadInfo struct { - Name string `json:"name"` - Size int `json:"size"` - CreatedAt string `json:"createdAt"` -} - -type FileTree struct { - ID string `json:"id"` - Name string `json:"name"` - Path string `json:"path"` - IsDir bool `json:"isDir"` - Extension string `json:"extension"` - Children []FileTree `json:"children"` -} - -type DirSizeRes struct { - Size float64 `json:"size" validate:"required"` -} - -type FileProcessKeys struct { - Keys []string `json:"keys"` -} - -type FileWgetRes struct { - Key string `json:"key"` -} - -type FileLineContent struct { - Content string `json:"content"` - End bool `json:"end"` - Path string `json:"path"` - Total int `json:"total"` -} - -type FileExist struct { - Exist bool `json:"exist"` -} diff --git a/backend/app/dto/response/host_tool.go b/backend/app/dto/response/host_tool.go deleted file mode 100644 index e97a675d8..000000000 --- a/backend/app/dto/response/host_tool.go +++ /dev/null @@ -1,41 +0,0 @@ -package response - -type HostToolRes struct { - Type string `json:"type"` - Config interface{} `json:"config"` -} - -type Supervisor struct { - ConfigPath string `json:"configPath"` - IncludeDir string `json:"includeDir"` - LogPath string `json:"logPath"` - IsExist bool `json:"isExist"` - Init bool `json:"init"` - Msg string `json:"msg"` - Version string `json:"version"` - Status string `json:"status"` - CtlExist bool `json:"ctlExist"` - ServiceName string `json:"serviceName"` -} - -type HostToolConfig struct { - Content string `json:"content"` -} - -type SupervisorProcessConfig struct { - Name string `json:"name"` - Command string `json:"command"` - User string `json:"user"` - Dir string `json:"dir"` - Numprocs string `json:"numprocs"` - Msg string `json:"msg"` - Status []ProcessStatus `json:"status"` -} - -type ProcessStatus struct { - Name string `json:"name"` - Status string `json:"status"` - PID string `json:"PID"` - Uptime string `json:"uptime"` - Msg string `json:"msg"` -} diff --git a/backend/app/dto/response/nginx.go b/backend/app/dto/response/nginx.go deleted file mode 100644 index c40521a68..000000000 --- a/backend/app/dto/response/nginx.go +++ /dev/null @@ -1,55 +0,0 @@ -package response - -import "github.com/1Panel-dev/1Panel/backend/app/dto" - -type NginxStatus struct { - Active string `json:"active"` - Accepts string `json:"accepts"` - Handled string `json:"handled"` - Requests string `json:"requests"` - Reading string `json:"reading"` - Writing string `json:"writing"` - Waiting string `json:"waiting"` -} - -type NginxParam struct { - Name string `json:"name"` - Params []string `json:"params"` -} - -type NginxAuthRes struct { - Enable bool `json:"enable"` - Items []dto.NginxAuth `json:"items"` -} - -type NginxAntiLeechRes struct { - Enable bool `json:"enable"` - Extends string `json:"extends"` - Return string `json:"return"` - ServerNames []string `json:"serverNames"` - Cache bool `json:"cache"` - CacheTime int `json:"cacheTime"` - CacheUint string `json:"cacheUint"` - NoneRef bool `json:"noneRef"` - LogEnable bool `json:"logEnable"` - Blocked bool `json:"blocked"` -} - -type NginxRedirectConfig struct { - WebsiteID uint `json:"websiteID"` - Name string `json:"name"` - Domains []string `json:"domains"` - KeepPath bool `json:"keepPath"` - Enable bool `json:"enable"` - Type string `json:"type"` - Redirect string `json:"redirect"` - Path string `json:"path"` - Target string `json:"target"` - FilePath string `json:"filePath"` - Content string `json:"content"` - RedirectRoot bool `json:"redirectRoot"` -} - -type NginxFile struct { - Content string `json:"content"` -} diff --git a/backend/app/dto/response/php_extensions.go b/backend/app/dto/response/php_extensions.go deleted file mode 100644 index 91a96f0d2..000000000 --- a/backend/app/dto/response/php_extensions.go +++ /dev/null @@ -1,7 +0,0 @@ -package response - -import "github.com/1Panel-dev/1Panel/backend/app/model" - -type PHPExtensionsDTO struct { - model.PHPExtensions -} diff --git a/backend/app/dto/response/recycle_bin.go b/backend/app/dto/response/recycle_bin.go deleted file mode 100644 index b5a040358..000000000 --- a/backend/app/dto/response/recycle_bin.go +++ /dev/null @@ -1,14 +0,0 @@ -package response - -import "time" - -type RecycleBinDTO struct { - Name string `json:"name"` - Size int `json:"size"` - Type string `json:"type"` - DeleteTime time.Time `json:"deleteTime"` - RName string `json:"rName"` - SourcePath string `json:"sourcePath"` - IsDir bool `json:"isDir"` - From string `json:"from"` -} diff --git a/backend/app/dto/response/runtime.go b/backend/app/dto/response/runtime.go deleted file mode 100644 index d743a24c9..000000000 --- a/backend/app/dto/response/runtime.go +++ /dev/null @@ -1,58 +0,0 @@ -package response - -import ( - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/app/model" - "time" -) - -type RuntimeDTO struct { - ID uint `json:"id"` - Name string `json:"name"` - Resource string `json:"resource"` - AppDetailID uint `json:"appDetailID"` - AppID uint `json:"appID"` - Source string `json:"source"` - Status string `json:"status"` - Type string `json:"type"` - Image string `json:"image"` - Params map[string]interface{} `json:"params"` - Message string `json:"message"` - Version string `json:"version"` - CreatedAt time.Time `json:"createdAt"` - CodeDir string `json:"codeDir"` - AppParams []AppParam `json:"appParams"` - Port int `json:"port"` - Path string `json:"path"` - ExposedPorts []request.ExposedPort `json:"exposedPorts"` -} - -type PackageScripts struct { - Name string `json:"name"` - Script string `json:"script"` -} - -func NewRuntimeDTO(runtime model.Runtime) RuntimeDTO { - return RuntimeDTO{ - ID: runtime.ID, - Name: runtime.Name, - Resource: runtime.Resource, - AppDetailID: runtime.AppDetailID, - Status: runtime.Status, - Type: runtime.Type, - Image: runtime.Image, - Message: runtime.Message, - CreatedAt: runtime.CreatedAt, - CodeDir: runtime.CodeDir, - Version: runtime.Version, - Port: runtime.Port, - Path: runtime.GetPath(), - } -} - -type NodeModule struct { - Name string `json:"name"` - Version string `json:"version"` - License string `json:"license"` - Description string `json:"description"` -} diff --git a/backend/app/dto/response/website.go b/backend/app/dto/response/website.go deleted file mode 100644 index 8a31b69e8..000000000 --- a/backend/app/dto/response/website.go +++ /dev/null @@ -1,88 +0,0 @@ -package response - -import ( - "github.com/1Panel-dev/1Panel/backend/app/model" - "time" -) - -type WebsiteDTO struct { - model.Website - ErrorLogPath string `json:"errorLogPath"` - AccessLogPath string `json:"accessLogPath"` - SitePath string `json:"sitePath"` - AppName string `json:"appName"` - RuntimeName string `json:"runtimeName"` - SiteDir string `gorm:"type:varchar;" json:"siteDir"` -} - -type WebsiteRes struct { - ID uint `json:"id"` - CreatedAt time.Time `json:"createdAt"` - Protocol string `json:"protocol"` - PrimaryDomain string `json:"primaryDomain"` - Type string `json:"type"` - Alias string `json:"alias"` - Remark string `json:"remark"` - Status string `json:"status"` - ExpireDate time.Time `json:"expireDate"` - SitePath string `json:"sitePath"` - AppName string `json:"appName"` - RuntimeName string `json:"runtimeName"` - SSLExpireDate time.Time `json:"sslExpireDate"` - SSLStatus string `json:"sslStatus"` -} - -type WebsiteOption struct { - ID uint `json:"id"` - PrimaryDomain string `json:"primaryDomain"` - Alias string `json:"alias"` -} - -type WebsitePreInstallCheck struct { - Name string `json:"name"` - Status string `json:"status"` - Version string `json:"version"` - AppName string `json:"appName"` -} - -type WebsiteNginxConfig struct { - Enable bool `json:"enable"` - Params []NginxParam `json:"params"` -} - -type WebsiteHTTPS struct { - Enable bool `json:"enable"` - HttpConfig string `json:"httpConfig"` - SSL model.WebsiteSSL `json:"SSL"` - SSLProtocol []string `json:"SSLProtocol"` - Algorithm string `json:"algorithm"` - Hsts bool `json:"hsts"` -} - -type WebsiteLog struct { - Enable bool `json:"enable"` - Content string `json:"content"` - End bool `json:"end"` - Path string `json:"path"` -} - -type PHPConfig struct { - Params map[string]string `json:"params"` - DisableFunctions []string `json:"disableFunctions"` - UploadMaxSize string `json:"uploadMaxSize"` -} - -type NginxRewriteRes struct { - Content string `json:"content"` -} - -type WebsiteDirConfig struct { - Dirs []string `json:"dirs"` - User string `json:"user"` - UserGroup string `json:"userGroup"` - Msg string `json:"msg"` -} - -type WebsiteHtmlRes struct { - Content string `json:"content"` -} diff --git a/backend/app/dto/response/website_ssl.go b/backend/app/dto/response/website_ssl.go deleted file mode 100644 index 1cb4062ce..000000000 --- a/backend/app/dto/response/website_ssl.go +++ /dev/null @@ -1,34 +0,0 @@ -package response - -import "github.com/1Panel-dev/1Panel/backend/app/model" - -type WebsiteSSLDTO struct { - model.WebsiteSSL - LogPath string `json:"logPath"` -} - -type WebsiteDNSRes struct { - Key string `json:"resolve"` - Value string `json:"value"` - Domain string `json:"domain"` - Err string `json:"err"` -} - -type WebsiteAcmeAccountDTO struct { - model.WebsiteAcmeAccount -} - -type WebsiteDnsAccountDTO struct { - model.WebsiteDnsAccount - Authorization map[string]string `json:"authorization"` -} - -type WebsiteCADTO struct { - model.WebsiteCA - CommonName string `json:"commonName" ` - Country string `json:"country"` - Organization string `json:"organization"` - OrganizationUint string `json:"organizationUint"` - Province string `json:"province" ` - City string `json:"city"` -} diff --git a/backend/app/dto/setting.go b/backend/app/dto/setting.go deleted file mode 100644 index 66c510c4e..000000000 --- a/backend/app/dto/setting.go +++ /dev/null @@ -1,124 +0,0 @@ -package dto - -import "time" - -type SettingInfo struct { - SystemIP string `json:"systemIP"` - DockerSockPath string `json:"dockerSockPath"` - SystemVersion string `json:"systemVersion"` - - LocalTime string `json:"localTime"` - TimeZone string `json:"timeZone"` - NtpSite string `json:"ntpSite"` - - DefaultNetwork string `json:"defaultNetwork"` - LastCleanTime string `json:"lastCleanTime"` - LastCleanSize string `json:"lastCleanSize"` - LastCleanData string `json:"lastCleanData"` - - MonitorStatus string `json:"monitorStatus"` - MonitorInterval string `json:"monitorInterval"` - MonitorStoreDays string `json:"monitorStoreDays"` - - AppStoreVersion string `json:"appStoreVersion"` - AppStoreLastModified string `json:"appStoreLastModified"` - AppStoreSyncStatus string `json:"appStoreSyncStatus"` - - FileRecycleBin string `json:"fileRecycleBin"` - - SnapshotIgnore string `json:"snapshotIgnore"` -} - -type SettingUpdate struct { - Key string `json:"key" validate:"required"` - Value string `json:"value"` -} - -type SnapshotStatus struct { - Panel string `json:"panel"` - PanelInfo string `json:"panelInfo"` - DaemonJson string `json:"daemonJson"` - AppData string `json:"appData"` - PanelData string `json:"panelData"` - BackupData string `json:"backupData"` - - Compress string `json:"compress"` - Size string `json:"size"` - Upload string `json:"upload"` -} - -type SnapshotCreate struct { - ID uint `json:"id"` - From string `json:"from" validate:"required"` - DefaultDownload string `json:"defaultDownload" validate:"required"` - Description string `json:"description" validate:"max=256"` - Secret string `json:"secret"` -} -type SnapshotRecover struct { - IsNew bool `json:"isNew"` - ReDownload bool `json:"reDownload"` - ID uint `json:"id" validate:"required"` - Secret string `json:"secret"` -} -type SnapshotBatchDelete struct { - DeleteWithFile bool `json:"deleteWithFile"` - Ids []uint `json:"ids" validate:"required"` -} - -type SnapshotImport struct { - From string `json:"from"` - Names []string `json:"names"` - Description string `json:"description" validate:"max=256"` -} - -type SnapshotInfo struct { - ID uint `json:"id"` - Name string `json:"name"` - Description string `json:"description" validate:"max=256"` - From string `json:"from"` - DefaultDownload string `json:"defaultDownload"` - Status string `json:"status"` - Message string `json:"message"` - CreatedAt time.Time `json:"createdAt"` - Version string `json:"version"` - Size int64 `json:"size"` - - InterruptStep string `json:"interruptStep"` - RecoverStatus string `json:"recoverStatus"` - RecoverMessage string `json:"recoverMessage"` - LastRecoveredAt string `json:"lastRecoveredAt"` - RollbackStatus string `json:"rollbackStatus"` - RollbackMessage string `json:"rollbackMessage"` - LastRollbackedAt string `json:"lastRollbackedAt"` -} - -type SyncTime struct { - NtpSite string `json:"ntpSite" validate:"required"` -} - -type CleanData struct { - SystemClean []CleanTree `json:"systemClean"` - UploadClean []CleanTree `json:"uploadClean"` - DownloadClean []CleanTree `json:"downloadClean"` - SystemLogClean []CleanTree `json:"systemLogClean"` - ContainerClean []CleanTree `json:"containerClean"` -} - -type CleanTree struct { - ID string `json:"id"` - Label string `json:"label"` - Children []CleanTree `json:"children"` - - Type string `json:"type"` - Name string `json:"name"` - - Size uint64 `json:"size"` - IsCheck bool `json:"isCheck"` - IsRecommend bool `json:"isRecommend"` -} - -type Clean struct { - TreeType string `json:"treeType"` - Name string `json:"name"` - Size uint64 `json:"size"` -} diff --git a/backend/app/dto/ssh.go b/backend/app/dto/ssh.go deleted file mode 100644 index c07a3f4e9..000000000 --- a/backend/app/dto/ssh.go +++ /dev/null @@ -1,57 +0,0 @@ -package dto - -import "time" - -type SSHUpdate struct { - Key string `json:"key" validate:"required"` - OldValue string `json:"oldValue"` - NewValue string `json:"newValue"` -} - -type SSHInfo struct { - AutoStart bool `json:"autoStart"` - Status string `json:"status"` - Message string `json:"message"` - Port string `json:"port"` - ListenAddress string `json:"listenAddress"` - PasswordAuthentication string `json:"passwordAuthentication"` - PubkeyAuthentication string `json:"pubkeyAuthentication"` - PermitRootLogin string `json:"permitRootLogin"` - UseDNS string `json:"useDNS"` -} - -type GenerateSSH struct { - EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"` - Password string `json:"password"` -} - -type GenerateLoad struct { - EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"` -} - -type SSHConf struct { - File string `json:"file"` -} -type SearchSSHLog struct { - PageInfo - Info string `json:"info"` - Status string `json:"Status" validate:"required,oneof=Success Failed All"` -} -type SSHLog struct { - Logs []SSHHistory `json:"logs"` - TotalCount int `json:"totalCount"` - SuccessfulCount int `json:"successfulCount"` - FailedCount int `json:"failedCount"` -} - -type SSHHistory struct { - Date time.Time `json:"date"` - DateStr string `json:"dateStr"` - Area string `json:"area"` - User string `json:"user"` - AuthMode string `json:"authMode"` - Address string `json:"address"` - Port string `json:"port"` - Status string `json:"status"` - Message string `json:"message"` -} diff --git a/backend/app/model/app.go b/backend/app/model/app.go deleted file mode 100644 index 3e67391e8..000000000 --- a/backend/app/model/app.go +++ /dev/null @@ -1,43 +0,0 @@ -package model - -import ( - "github.com/1Panel-dev/1Panel/backend/constant" - "path/filepath" - "strings" -) - -type App struct { - BaseModel - Name string `json:"name" gorm:"type:varchar(64);not null"` - Key string `json:"key" gorm:"type:varchar(64);not null;"` - ShortDescZh string `json:"shortDescZh" yaml:"shortDescZh" gorm:"type:longtext;"` - ShortDescEn string `json:"shortDescEn" yaml:"shortDescEn" gorm:"type:longtext;"` - Icon string `json:"icon" gorm:"type:longtext;"` - Type string `json:"type" gorm:"type:varchar(64);not null"` - Status string `json:"status" gorm:"type:varchar(64);not null"` - Required string `json:"required" gorm:"type:varchar(64);"` - CrossVersionUpdate bool `json:"crossVersionUpdate" yaml:"crossVersionUpdate"` - Limit int `json:"limit" gorm:"type:Integer;not null"` - Website string `json:"website" gorm:"type:varchar(64);not null"` - Github string `json:"github" gorm:"type:varchar(64);not null"` - Document string `json:"document" gorm:"type:varchar(64);not null"` - Recommend int `json:"recommend" gorm:"type:Integer;not null"` - Resource string `json:"resource" gorm:"type:varchar;not null;default:remote"` - ReadMe string `json:"readMe" gorm:"type:varchar;"` - LastModified int `json:"lastModified" gorm:"type:Integer;"` - - Details []AppDetail `json:"-" gorm:"-:migration"` - TagsKey []string `json:"tags" yaml:"tags" gorm:"-"` - AppTags []AppTag `json:"-" gorm:"-:migration"` -} - -func (i *App) IsLocalApp() bool { - return i.Resource == constant.ResourceLocal -} -func (i *App) GetAppResourcePath() string { - if i.IsLocalApp() { - //这里要去掉本地应用的local前缀 - return filepath.Join(constant.LocalAppResourceDir, strings.TrimPrefix(i.Key, "local")) - } - return filepath.Join(constant.RemoteAppResourceDir, i.Key) -} diff --git a/backend/app/model/app_detail.go b/backend/app/model/app_detail.go deleted file mode 100644 index d6b958284..000000000 --- a/backend/app/model/app_detail.go +++ /dev/null @@ -1,16 +0,0 @@ -package model - -type AppDetail struct { - BaseModel - AppId uint `json:"appId" gorm:"type:integer;not null"` - Version string `json:"version" gorm:"type:varchar(64);not null"` - Params string `json:"-" gorm:"type:longtext;"` - DockerCompose string `json:"dockerCompose" gorm:"type:longtext;"` - Status string `json:"status" gorm:"type:varchar(64);not null"` - LastVersion string `json:"lastVersion" gorm:"type:varchar(64);"` - LastModified int `json:"lastModified" gorm:"type:integer;"` - DownloadUrl string `json:"downloadUrl" gorm:"type:varchar;"` - DownloadCallBackUrl string `json:"downloadCallBackUrl" gorm:"type:longtext;"` - Update bool `json:"update"` - IgnoreUpgrade bool `json:"ignoreUpgrade"` -} diff --git a/backend/app/model/app_install.go b/backend/app/model/app_install.go deleted file mode 100644 index 539b131ba..000000000 --- a/backend/app/model/app_install.go +++ /dev/null @@ -1,47 +0,0 @@ -package model - -import ( - "path" - "strings" - - "github.com/1Panel-dev/1Panel/backend/constant" -) - -type AppInstall struct { - BaseModel - Name string `json:"name" gorm:"type:varchar(64);not null;UNIQUE"` - AppId uint `json:"appId" gorm:"type:integer;not null"` - AppDetailId uint `json:"appDetailId" gorm:"type:integer;not null"` - Version string `json:"version" gorm:"type:varchar(64);not null"` - Param string `json:"param" gorm:"type:longtext;"` - Env string `json:"env" gorm:"type:longtext;"` - DockerCompose string `json:"dockerCompose" gorm:"type:longtext;"` - Status string `json:"status" gorm:"type:varchar(256);not null"` - Description string `json:"description" gorm:"type:varchar(256);"` - Message string `json:"message" gorm:"type:longtext;"` - ContainerName string `json:"containerName" gorm:"type:varchar(256);not null"` - ServiceName string `json:"serviceName" gorm:"type:varchar(256);not null"` - HttpPort int `json:"httpPort" gorm:"type:integer;not null"` - HttpsPort int `json:"httpsPort" gorm:"type:integer;not null"` - App App `json:"app" gorm:"-:migration"` -} - -func (i *AppInstall) GetPath() string { - return path.Join(i.GetAppPath(), i.Name) -} - -func (i *AppInstall) GetComposePath() string { - return path.Join(i.GetAppPath(), i.Name, "docker-compose.yml") -} - -func (i *AppInstall) GetEnvPath() string { - return path.Join(i.GetAppPath(), i.Name, ".env") -} - -func (i *AppInstall) GetAppPath() string { - if i.App.Resource == constant.AppResourceLocal { - return path.Join(constant.LocalAppInstallDir, strings.TrimPrefix(i.App.Key, constant.AppResourceLocal)) - } else { - return path.Join(constant.AppInstallDir, i.App.Key) - } -} diff --git a/backend/app/model/app_install_resource.go b/backend/app/model/app_install_resource.go deleted file mode 100644 index 863090459..000000000 --- a/backend/app/model/app_install_resource.go +++ /dev/null @@ -1,10 +0,0 @@ -package model - -type AppInstallResource struct { - BaseModel - AppInstallId uint `json:"appInstallId" gorm:"type:integer;not null;"` - LinkId uint `json:"linkId" gorm:"type:integer;not null;"` - ResourceId uint `json:"resourceId" gorm:"type:integer;"` - Key string `json:"key" gorm:"type:varchar(64);not null"` - From string `json:"from" gorm:"type:varchar(64);not null;default:local"` -} diff --git a/backend/app/model/app_tag.go b/backend/app/model/app_tag.go deleted file mode 100644 index 1d55b956a..000000000 --- a/backend/app/model/app_tag.go +++ /dev/null @@ -1,7 +0,0 @@ -package model - -type AppTag struct { - BaseModel - AppId uint `json:"appId" gorm:"type:integer;not null"` - TagId uint `json:"tagId" gorm:"type:integer;not null"` -} diff --git a/backend/app/model/backup.go b/backend/app/model/backup.go deleted file mode 100644 index 50690dcaa..000000000 --- a/backend/app/model/backup.go +++ /dev/null @@ -1,24 +0,0 @@ -package model - -type BackupAccount struct { - BaseModel - Type string `gorm:"type:varchar(64);unique;not null" json:"type"` - Bucket string `gorm:"type:varchar(256)" json:"bucket"` - AccessKey string `gorm:"type:varchar(256)" json:"accessKey"` - Credential string `gorm:"type:varchar(256)" json:"credential"` - BackupPath string `gorm:"type:varchar(256)" json:"backupPath"` - Vars string `gorm:"type:longText" json:"vars"` -} - -type BackupRecord struct { - BaseModel - From string `gorm:"type:varchar(64)" json:"from"` - CronjobID uint `gorm:"type:decimal" json:"cronjobID"` - Type string `gorm:"type:varchar(64);not null" json:"type"` - Name string `gorm:"type:varchar(64);not null" json:"name"` - DetailName string `gorm:"type:varchar(256)" json:"detailName"` - Source string `gorm:"type:varchar(256)" json:"source"` - BackupType string `gorm:"type:varchar(256)" json:"backupType"` - FileDir string `gorm:"type:varchar(256)" json:"fileDir"` - FileName string `gorm:"type:varchar(256)" json:"fileName"` -} diff --git a/backend/app/model/base.go b/backend/app/model/base.go deleted file mode 100644 index 69921d41d..000000000 --- a/backend/app/model/base.go +++ /dev/null @@ -1,9 +0,0 @@ -package model - -import "time" - -type BaseModel struct { - ID uint `gorm:"primarykey;AUTO_INCREMENT" json:"id"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` -} diff --git a/backend/app/model/clam.go b/backend/app/model/clam.go deleted file mode 100644 index ff1a73e67..000000000 --- a/backend/app/model/clam.go +++ /dev/null @@ -1,14 +0,0 @@ -package model - -type Clam struct { - BaseModel - - Name string `gorm:"type:varchar(64);not null" json:"name"` - Status string `gorm:"type:varchar(64)" json:"status"` - Path string `gorm:"type:varchar(64);not null" json:"path"` - InfectedStrategy string `gorm:"type:varchar(64)" json:"infectedStrategy"` - InfectedDir string `gorm:"type:varchar(64)" json:"infectedDir"` - Spec string `gorm:"type:varchar(64)" json:"spec"` - EntryID int `gorm:"type:varchar(64)" json:"entryID"` - Description string `gorm:"type:varchar(64)" json:"description"` -} diff --git a/backend/app/model/command.go b/backend/app/model/command.go deleted file mode 100644 index 9f00558d4..000000000 --- a/backend/app/model/command.go +++ /dev/null @@ -1,14 +0,0 @@ -package model - -type Command struct { - BaseModel - Name string `gorm:"type:varchar(64);unique;not null" json:"name"` - GroupID uint `gorm:"type:decimal" json:"groupID"` - Command string `gorm:"type:varchar(256);not null" json:"command"` -} - -type RedisCommand struct { - BaseModel - Name string `gorm:"type:varchar(64);unique;not null" json:"name"` - Command string `gorm:"type:varchar(256);not null" json:"command"` -} diff --git a/backend/app/model/compose_template.go b/backend/app/model/compose_template.go deleted file mode 100644 index 5d5192eb3..000000000 --- a/backend/app/model/compose_template.go +++ /dev/null @@ -1,15 +0,0 @@ -package model - -type ComposeTemplate struct { - BaseModel - - Name string `gorm:"type:varchar(64);not null;unique" json:"name"` - Description string `gorm:"type:varchar(256)" json:"description"` - Content string `gorm:"type:longtext" json:"content"` -} - -type Compose struct { - BaseModel - - Name string `gorm:"type:varchar(256)" json:"name"` -} diff --git a/backend/app/model/cronjob.go b/backend/app/model/cronjob.go deleted file mode 100644 index b9ff73546..000000000 --- a/backend/app/model/cronjob.go +++ /dev/null @@ -1,50 +0,0 @@ -package model - -import ( - "time" -) - -type Cronjob struct { - BaseModel - - Name string `gorm:"type:varchar(64);not null" json:"name"` - Type string `gorm:"type:varchar(64);not null" json:"type"` - Spec string `gorm:"type:varchar(64);not null" json:"spec"` - - Command string `gorm:"type:varchar(64)" json:"command"` - ContainerName string `gorm:"type:varchar(64)" json:"containerName"` - Script string `gorm:"longtext" json:"script"` - Website string `gorm:"type:varchar(64)" json:"website"` - AppID string `gorm:"type:varchar(64)" json:"appID"` - DBType string `gorm:"type:varchar(64)" json:"dbType"` - DBName string `gorm:"type:varchar(64)" json:"dbName"` - URL string `gorm:"type:varchar(256)" json:"url"` - SourceDir string `gorm:"type:varchar(256)" json:"sourceDir"` - ExclusionRules string `gorm:"longtext" json:"exclusionRules"` - - // 已废弃 - KeepLocal bool `gorm:"type:varchar(64)" json:"keepLocal"` - TargetDirID uint64 `gorm:"type:decimal" json:"targetDirID"` - - BackupAccounts string `gorm:"type:varchar(64)" json:"backupAccounts"` - DefaultDownload string `gorm:"type:varchar(64)" json:"defaultDownload"` - RetainCopies uint64 `gorm:"type:decimal" json:"retainCopies"` - - Status string `gorm:"type:varchar(64)" json:"status"` - EntryIDs string `gorm:"type:varchar(64)" json:"entryIDs"` - Records []JobRecords `json:"records"` - Secret string `gorm:"type:varchar(64)" json:"secret"` -} - -type JobRecords struct { - BaseModel - - CronjobID uint `gorm:"type:decimal" json:"cronjobID"` - StartTime time.Time `gorm:"type:datetime" json:"startTime"` - Interval float64 `gorm:"type:float" json:"interval"` - Records string `gorm:"longtext" json:"records"` - FromLocal bool `gorm:"type:varchar(64)" json:"source"` - File string `gorm:"type:varchar(256)" json:"file"` - Status string `gorm:"type:varchar(64)" json:"status"` - Message string `gorm:"longtext" json:"message"` -} diff --git a/backend/app/model/database.go b/backend/app/model/database.go deleted file mode 100644 index f4d0ff0fc..000000000 --- a/backend/app/model/database.go +++ /dev/null @@ -1,22 +0,0 @@ -package model - -type Database struct { - BaseModel - AppInstallID uint `json:"appInstallID" gorm:"type:decimal"` - Name string `json:"name" gorm:"type:varchar(64);not null;unique"` - Type string `json:"type" gorm:"type:varchar(64);not null"` - Version string `json:"version" gorm:"type:varchar(64);not null"` - From string `json:"from" gorm:"type:varchar(64);not null"` - Address string `json:"address" gorm:"type:varchar(64);not null"` - Port uint `json:"port" gorm:"type:decimal;not null"` - Username string `json:"username" gorm:"type:varchar(64)"` - Password string `json:"password" gorm:"type:varchar(64)"` - - SSL bool `json:"ssl"` - RootCert string `json:"rootCert" gorm:"type:longText"` - ClientKey string `json:"clientKey" gorm:"type:longText"` - ClientCert string `json:"clientCert" gorm:"type:longText"` - SkipVerify bool `json:"skipVerify"` - - Description string `json:"description" gorm:"type:varchar(256);"` -} diff --git a/backend/app/model/database_mysql.go b/backend/app/model/database_mysql.go deleted file mode 100644 index 0a8734848..000000000 --- a/backend/app/model/database_mysql.go +++ /dev/null @@ -1,14 +0,0 @@ -package model - -type DatabaseMysql struct { - BaseModel - Name string `json:"name" gorm:"type:varchar(256);not null"` - From string `json:"from" gorm:"type:varchar(256);not null;default:local"` - MysqlName string `json:"mysqlName" gorm:"type:varchar(64);not null"` - Format string `json:"format" gorm:"type:varchar(64);not null"` - Username string `json:"username" gorm:"type:varchar(256);not null"` - Password string `json:"password" gorm:"type:varchar(256);not null"` - Permission string `json:"permission" gorm:"type:varchar(256);not null"` - IsDelete bool `json:"isDelete" gorm:"type:varchar(64)"` - Description string `json:"description" gorm:"type:varchar(256);"` -} diff --git a/backend/app/model/database_postgresql.go b/backend/app/model/database_postgresql.go deleted file mode 100644 index 7ea4ee59e..000000000 --- a/backend/app/model/database_postgresql.go +++ /dev/null @@ -1,14 +0,0 @@ -package model - -type DatabasePostgresql struct { - BaseModel - Name string `json:"name" gorm:"type:varchar(256);not null"` - From string `json:"from" gorm:"type:varchar(256);not null;default:local"` - PostgresqlName string `json:"postgresqlName" gorm:"type:varchar(64);not null"` - Format string `json:"format" gorm:"type:varchar(64);not null"` - Username string `json:"username" gorm:"type:varchar(256);not null"` - Password string `json:"password" gorm:"type:varchar(256);not null"` - SuperUser bool `json:"superUser" gorm:"type:varchar(64)"` - IsDelete bool `json:"isDelete" gorm:"type:varchar(64)"` - Description string `json:"description" gorm:"type:varchar(256);"` -} diff --git a/backend/app/model/favorite.go b/backend/app/model/favorite.go deleted file mode 100644 index 6e5421dcf..000000000 --- a/backend/app/model/favorite.go +++ /dev/null @@ -1,10 +0,0 @@ -package model - -type Favorite struct { - BaseModel - Name string `gorm:"type:varchar(256);not null;" json:"name" ` - Path string `gorm:"type:varchar(256);not null;unique" json:"path"` - Type string `gorm:"type:varchar(64);" json:"type"` - IsDir bool `json:"isDir"` - IsTxt bool `json:"isTxt"` -} diff --git a/backend/app/model/firewall.go b/backend/app/model/firewall.go deleted file mode 100644 index d4979a05d..000000000 --- a/backend/app/model/firewall.go +++ /dev/null @@ -1,21 +0,0 @@ -package model - -type Firewall struct { - BaseModel - - Type string `gorm:"type:varchar(64);not null" json:"type"` - Port string `gorm:"type:varchar(64);not null" json:"port"` - Protocol string `gorm:"type:varchar(64);not null" json:"protocol"` - Address string `gorm:"type:varchar(64);not null" json:"address"` - Strategy string `gorm:"type:varchar(64);not null" json:"strategy"` - Description string `gorm:"type:varchar(64);not null" json:"description"` -} - -type Forward struct { - BaseModel - - Protocol string `gorm:"type:varchar(64);not null" json:"protocol"` - Port string `gorm:"type:varchar(64);not null" json:"port"` - TargetIP string `gorm:"type:varchar(64);not null" json:"targetIP"` - TargetPort string `gorm:"type:varchar(64);not null" json:"targetPort"` -} diff --git a/backend/app/model/ftp.go b/backend/app/model/ftp.go deleted file mode 100644 index 291e765c6..000000000 --- a/backend/app/model/ftp.go +++ /dev/null @@ -1,11 +0,0 @@ -package model - -type Ftp struct { - BaseModel - - User string `gorm:"type:varchar(64);not null" json:"user"` - Password string `gorm:"type:varchar(64);not null" json:"password"` - Status string `gorm:"type:varchar(64);not null" json:"status"` - Path string `gorm:"type:varchar(64);not null" json:"path"` - Description string `gorm:"type:varchar(64);not null" json:"description"` -} diff --git a/backend/app/model/group.go b/backend/app/model/group.go deleted file mode 100644 index 1479e035a..000000000 --- a/backend/app/model/group.go +++ /dev/null @@ -1,8 +0,0 @@ -package model - -type Group struct { - BaseModel - IsDefault bool `json:"isDefault"` - Name string `gorm:"type:varchar(64);not null" json:"name"` - Type string `gorm:"type:varchar(16);not null" json:"type"` -} diff --git a/backend/app/model/host.go b/backend/app/model/host.go deleted file mode 100644 index a4fe6aedd..000000000 --- a/backend/app/model/host.go +++ /dev/null @@ -1,18 +0,0 @@ -package model - -type Host struct { - BaseModel - - GroupID uint `gorm:"type:decimal;not null" json:"group_id"` - Name string `gorm:"type:varchar(64);not null" json:"name"` - Addr string `gorm:"type:varchar(16);not null" json:"addr"` - Port int `gorm:"type:decimal;not null" json:"port"` - User string `gorm:"type:varchar(64);not null" json:"user"` - AuthMode string `gorm:"type:varchar(16);not null" json:"authMode"` - Password string `gorm:"type:varchar(64)" json:"password"` - PrivateKey string `gorm:"type:varchar(256)" json:"privateKey"` - PassPhrase string `gorm:"type:varchar(256)" json:"passPhrase"` - RememberPassword bool `json:"rememberPassword"` - - Description string `gorm:"type:varchar(256)" json:"description"` -} diff --git a/backend/app/model/image_repo.go b/backend/app/model/image_repo.go deleted file mode 100644 index 8d16dbff1..000000000 --- a/backend/app/model/image_repo.go +++ /dev/null @@ -1,15 +0,0 @@ -package model - -type ImageRepo struct { - BaseModel - - Name string `gorm:"type:varchar(64);not null" json:"name"` - DownloadUrl string `gorm:"type:varchar(256)" json:"downloadUrl"` - Protocol string `gorm:"type:varchar(64)" json:"protocol"` - Username string `gorm:"type:varchar(256)" json:"username"` - Password string `gorm:"type:varchar(256)" json:"password"` - Auth bool `gorm:"type:varchar(256)" json:"auth"` - - Status string `gorm:"type:varchar(64)" json:"status"` - Message string `gorm:"type:varchar(256)" json:"message"` -} diff --git a/backend/app/model/monitor.go b/backend/app/model/monitor.go deleted file mode 100644 index 1207c98af..000000000 --- a/backend/app/model/monitor.go +++ /dev/null @@ -1,29 +0,0 @@ -package model - -type MonitorBase struct { - BaseModel - Cpu float64 `gorm:"type:float" json:"cpu"` - - LoadUsage float64 `gorm:"type:float" json:"loadUsage"` - CpuLoad1 float64 `gorm:"type:float" json:"cpuLoad1"` - CpuLoad5 float64 `gorm:"type:float" json:"cpuLoad5"` - CpuLoad15 float64 `gorm:"type:float" json:"cpuLoad15"` - - Memory float64 `gorm:"type:float" json:"memory"` -} - -type MonitorIO struct { - BaseModel - Name string `json:"name"` - Read uint64 `json:"read"` - Write uint64 `json:"write"` - Count uint64 `json:"count"` - Time uint64 `json:"time"` -} - -type MonitorNetwork struct { - BaseModel - Name string `json:"name"` - Up float64 `gorm:"type:float" json:"up"` - Down float64 `gorm:"type:float" json:"down"` -} diff --git a/backend/app/model/php_extensions.go b/backend/app/model/php_extensions.go deleted file mode 100644 index 0055bd258..000000000 --- a/backend/app/model/php_extensions.go +++ /dev/null @@ -1,7 +0,0 @@ -package model - -type PHPExtensions struct { - BaseModel - Name string ` json:"name" gorm:"not null"` - Extensions string `json:"extensions" gorm:"not null"` -} diff --git a/backend/app/model/runtime.go b/backend/app/model/runtime.go deleted file mode 100644 index d71051a44..000000000 --- a/backend/app/model/runtime.go +++ /dev/null @@ -1,40 +0,0 @@ -package model - -import ( - "github.com/1Panel-dev/1Panel/backend/constant" - "path" -) - -type Runtime struct { - BaseModel - Name string `gorm:"type:varchar;not null" json:"name"` - AppDetailID uint `gorm:"type:integer" json:"appDetailId"` - Image string `gorm:"type:varchar" json:"image"` - WorkDir string `gorm:"type:varchar" json:"workDir"` - DockerCompose string `gorm:"type:varchar" json:"dockerCompose"` - Env string `gorm:"type:varchar" json:"env"` - Params string `gorm:"type:varchar" json:"params"` - Version string `gorm:"type:varchar;not null" json:"version"` - Type string `gorm:"type:varchar;not null" json:"type"` - Status string `gorm:"type:varchar;not null" json:"status"` - Resource string `gorm:"type:varchar;not null" json:"resource"` - Port int `gorm:"type:integer;" json:"port"` - Message string `gorm:"type:longtext;" json:"message"` - CodeDir string `gorm:"type:varchar;" json:"codeDir"` -} - -func (r *Runtime) GetComposePath() string { - return path.Join(r.GetPath(), "docker-compose.yml") -} - -func (r *Runtime) GetEnvPath() string { - return path.Join(r.GetPath(), ".env") -} - -func (r *Runtime) GetPath() string { - return path.Join(constant.RuntimeDir, r.Type, r.Name) -} - -func (r *Runtime) GetLogPath() string { - return path.Join(r.GetPath(), "build.log") -} diff --git a/backend/app/model/setting.go b/backend/app/model/setting.go deleted file mode 100644 index e9791b79b..000000000 --- a/backend/app/model/setting.go +++ /dev/null @@ -1,8 +0,0 @@ -package model - -type Setting struct { - BaseModel - Key string `json:"key" gorm:"type:varchar(256);not null;"` - Value string `json:"value" gorm:"type:varchar(256)"` - About string `json:"about" gorm:"type:longText"` -} diff --git a/backend/app/model/snapshot.go b/backend/app/model/snapshot.go deleted file mode 100644 index 09574ca65..000000000 --- a/backend/app/model/snapshot.go +++ /dev/null @@ -1,35 +0,0 @@ -package model - -type Snapshot struct { - BaseModel - Name string `json:"name" gorm:"type:varchar(64);not null;unique"` - Description string `json:"description" gorm:"type:varchar(256)"` - From string `json:"from"` - DefaultDownload string `json:"defaultDownload" gorm:"type:varchar(64)"` - Status string `json:"status" gorm:"type:varchar(64)"` - Message string `json:"message" gorm:"type:varchar(256)"` - Version string `json:"version" gorm:"type:varchar(256)"` - - InterruptStep string `json:"interruptStep" gorm:"type:varchar(64)"` - RecoverStatus string `json:"recoverStatus" gorm:"type:varchar(64)"` - RecoverMessage string `json:"recoverMessage" gorm:"type:varchar(256)"` - LastRecoveredAt string `json:"lastRecoveredAt" gorm:"type:varchar(64)"` - RollbackStatus string `json:"rollbackStatus" gorm:"type:varchar(64)"` - RollbackMessage string `json:"rollbackMessage" gorm:"type:varchar(256)"` - LastRollbackedAt string `json:"lastRollbackedAt" gorm:"type:varchar(64)"` -} - -type SnapshotStatus struct { - BaseModel - SnapID uint `gorm:"type:decimal" json:"snapID"` - Panel string `json:"panel" gorm:"type:varchar(64);default:Running"` - PanelInfo string `json:"panelInfo" gorm:"type:varchar(64);default:Running"` - DaemonJson string `json:"daemonJson" gorm:"type:varchar(64);default:Running"` - AppData string `json:"appData" gorm:"type:varchar(64);default:Running"` - PanelData string `json:"panelData" gorm:"type:varchar(64);default:Running"` - BackupData string `json:"backupData" gorm:"type:varchar(64);default:Running"` - - Compress string `json:"compress" gorm:"type:varchar(64);default:Waiting"` - Size string `json:"size" gorm:"type:varchar(64)"` - Upload string `json:"upload" gorm:"type:varchar(64);default:Waiting"` -} diff --git a/backend/app/model/tag.go b/backend/app/model/tag.go deleted file mode 100644 index adc8d6ec4..000000000 --- a/backend/app/model/tag.go +++ /dev/null @@ -1,8 +0,0 @@ -package model - -type Tag struct { - BaseModel - Key string `json:"key" gorm:"type:varchar(64);not null"` - Name string `json:"name" gorm:"type:varchar(64);not null"` - Sort int `json:"sort" gorm:"type:int;not null;default:1"` -} diff --git a/backend/app/model/website.go b/backend/app/model/website.go deleted file mode 100644 index 34dc72ab4..000000000 --- a/backend/app/model/website.go +++ /dev/null @@ -1,40 +0,0 @@ -package model - -import "time" - -type Website struct { - BaseModel - Protocol string `gorm:"type:varchar;not null" json:"protocol"` - PrimaryDomain string `gorm:"type:varchar;not null" json:"primaryDomain"` - Type string `gorm:"type:varchar;not null" json:"type"` - Alias string `gorm:"type:varchar;not null" json:"alias"` - Remark string `gorm:"type:longtext;" json:"remark"` - Status string `gorm:"type:varchar;not null" json:"status"` - HttpConfig string `gorm:"type:varchar;not null" json:"httpConfig"` - ExpireDate time.Time `json:"expireDate"` - - Proxy string `gorm:"type:varchar;" json:"proxy"` - ProxyType string `gorm:"type:varchar;" json:"proxyType"` - SiteDir string `gorm:"type:varchar;" json:"siteDir"` - ErrorLog bool `json:"errorLog"` - AccessLog bool `json:"accessLog"` - DefaultServer bool `json:"defaultServer"` - IPV6 bool `json:"IPV6"` - Rewrite string `gorm:"type:varchar" json:"rewrite"` - - WebsiteGroupID uint `gorm:"type:integer" json:"webSiteGroupId"` - WebsiteSSLID uint `gorm:"type:integer" json:"webSiteSSLId"` - RuntimeID uint `gorm:"type:integer" json:"runtimeID"` - AppInstallID uint `gorm:"type:integer" json:"appInstallId"` - FtpID uint `gorm:"type:integer" json:"ftpId"` - - User string `gorm:"type:varchar;" json:"user"` - Group string `gorm:"type:varchar;" json:"group"` - - Domains []WebsiteDomain `json:"domains" gorm:"-:migration"` - WebsiteSSL WebsiteSSL `json:"webSiteSSL" gorm:"-:migration"` -} - -func (w Website) TableName() string { - return "websites" -} diff --git a/backend/app/model/website_acme_account.go b/backend/app/model/website_acme_account.go deleted file mode 100644 index eeafc0187..000000000 --- a/backend/app/model/website_acme_account.go +++ /dev/null @@ -1,16 +0,0 @@ -package model - -type WebsiteAcmeAccount struct { - BaseModel - Email string `gorm:"not null" json:"email"` - URL string `gorm:"not null" json:"url"` - PrivateKey string `gorm:"not null" json:"-"` - Type string `gorm:"not null;default:letsencrypt" json:"type"` - EabKid string `gorm:"default:null;" json:"eabKid"` - EabHmacKey string `gorm:"default:null" json:"eabHmacKey"` - KeyType string `gorm:"not null;default:2048" json:"keyType"` -} - -func (w WebsiteAcmeAccount) TableName() string { - return "website_acme_accounts" -} diff --git a/backend/app/model/website_ca.go b/backend/app/model/website_ca.go deleted file mode 100644 index 74b029637..000000000 --- a/backend/app/model/website_ca.go +++ /dev/null @@ -1,9 +0,0 @@ -package model - -type WebsiteCA struct { - BaseModel - CSR string `gorm:"not null;" json:"csr"` - Name string `gorm:"not null;" json:"name"` - PrivateKey string `gorm:"not null" json:"privateKey"` - KeyType string `gorm:"not null;default:2048" json:"keyType"` -} diff --git a/backend/app/model/website_dns_account.go b/backend/app/model/website_dns_account.go deleted file mode 100644 index de7ae8dee..000000000 --- a/backend/app/model/website_dns_account.go +++ /dev/null @@ -1,12 +0,0 @@ -package model - -type WebsiteDnsAccount struct { - BaseModel - Name string `gorm:"type:varchar(64);not null" json:"name"` - Type string `gorm:"type:varchar(64);not null" json:"type"` - Authorization string `gorm:"type:varchar(256);not null" json:"-"` -} - -func (w WebsiteDnsAccount) TableName() string { - return "website_dns_accounts" -} diff --git a/backend/app/model/website_domain.go b/backend/app/model/website_domain.go deleted file mode 100644 index e6fb77e93..000000000 --- a/backend/app/model/website_domain.go +++ /dev/null @@ -1,12 +0,0 @@ -package model - -type WebsiteDomain struct { - BaseModel - WebsiteID uint `gorm:"column:website_id;type:varchar(64);not null;" json:"websiteId"` - Domain string `gorm:"type:varchar(256);not null" json:"domain"` - Port int `gorm:"type:integer" json:"port"` -} - -func (w WebsiteDomain) TableName() string { - return "website_domains" -} diff --git a/backend/app/model/website_ssl.go b/backend/app/model/website_ssl.go deleted file mode 100644 index 15fbb8c4b..000000000 --- a/backend/app/model/website_ssl.go +++ /dev/null @@ -1,50 +0,0 @@ -package model - -import ( - "fmt" - "github.com/1Panel-dev/1Panel/backend/constant" - "path" - "time" -) - -type WebsiteSSL struct { - BaseModel - PrimaryDomain string `json:"primaryDomain"` - PrivateKey string `json:"privateKey"` - Pem string `json:"pem"` - Domains string `json:"domains"` - CertURL string `json:"certURL"` - Type string `json:"type"` - Provider string `json:"provider"` - Organization string `json:"organization"` - DnsAccountID uint `json:"dnsAccountId"` - AcmeAccountID uint `gorm:"column:acme_account_id" json:"acmeAccountId" ` - CaID uint `json:"caId"` - AutoRenew bool `json:"autoRenew"` - ExpireDate time.Time `json:"expireDate"` - StartDate time.Time `json:"startDate"` - Status string `json:"status"` - Message string `json:"message"` - KeyType string `json:"keyType"` - PushDir bool `json:"pushDir"` - Dir string `json:"dir"` - Description string `json:"description"` - SkipDNS bool `json:"skipDNS"` - Nameserver1 string `json:"nameserver1"` - Nameserver2 string `json:"nameserver2"` - DisableCNAME bool `json:"disableCNAME"` - ExecShell bool `json:"execShell"` - Shell string `json:"shell"` - - AcmeAccount WebsiteAcmeAccount `json:"acmeAccount" gorm:"-:migration"` - DnsAccount WebsiteDnsAccount `json:"dnsAccount" gorm:"-:migration"` - Websites []Website `json:"websites" gorm:"-:migration"` -} - -func (w WebsiteSSL) TableName() string { - return "website_ssls" -} - -func (w WebsiteSSL) GetLogPath() string { - return path.Join(constant.SSLLogDir, fmt.Sprintf("%s-ssl-%d.log", w.PrimaryDomain, w.ID)) -} diff --git a/backend/app/repo/app.go b/backend/app/repo/app.go deleted file mode 100644 index 9eb0c8ece..000000000 --- a/backend/app/repo/app.go +++ /dev/null @@ -1,123 +0,0 @@ -package repo - -import ( - "context" - - "github.com/1Panel-dev/1Panel/backend/app/model" - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -type AppRepo struct { -} - -type IAppRepo interface { - WithKey(key string) DBOption - WithType(typeStr string) DBOption - OrderByRecommend() DBOption - GetRecommend() DBOption - WithResource(resource string) DBOption - WithLikeName(name string) DBOption - Page(page, size int, opts ...DBOption) (int64, []model.App, error) - GetFirst(opts ...DBOption) (model.App, error) - GetBy(opts ...DBOption) ([]model.App, error) - BatchCreate(ctx context.Context, apps []model.App) error - GetByKey(ctx context.Context, key string) (model.App, error) - Create(ctx context.Context, app *model.App) error - Save(ctx context.Context, app *model.App) error - BatchDelete(ctx context.Context, apps []model.App) error -} - -func NewIAppRepo() IAppRepo { - return &AppRepo{} -} - -func (a AppRepo) WithLikeName(name string) DBOption { - return func(g *gorm.DB) *gorm.DB { - if len(name) == 0 { - return g - } - return g.Where("name like ? or short_desc_zh like ? or short_desc_en like ?", "%"+name+"%", "%"+name+"%", "%"+name+"%") - } -} - -func (a AppRepo) WithKey(key string) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("key = ?", key) - } -} - -func (a AppRepo) WithType(typeStr string) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("type = ?", typeStr) - } -} - -func (a AppRepo) OrderByRecommend() DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Order("recommend asc") - } -} - -func (a AppRepo) GetRecommend() DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("recommend < 9999") - } -} - -func (a AppRepo) WithResource(resource string) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("resource = ?", resource) - } -} - -func (a AppRepo) Page(page, size int, opts ...DBOption) (int64, []model.App, error) { - var apps []model.App - db := getDb(opts...).Model(&model.App{}) - count := int64(0) - db = db.Count(&count) - err := db.Limit(size).Offset(size * (page - 1)).Preload("AppTags").Find(&apps).Error - return count, apps, err -} - -func (a AppRepo) GetFirst(opts ...DBOption) (model.App, error) { - var app model.App - db := getDb(opts...).Model(&model.App{}) - if err := db.Preload("AppTags").First(&app).Error; err != nil { - return app, err - } - return app, nil -} - -func (a AppRepo) GetBy(opts ...DBOption) ([]model.App, error) { - var apps []model.App - db := getDb(opts...).Model(&model.App{}) - if err := db.Preload("Details").Preload("AppTags").Find(&apps).Error; err != nil { - return apps, err - } - return apps, nil -} - -func (a AppRepo) BatchCreate(ctx context.Context, apps []model.App) error { - return getTx(ctx).Omit(clause.Associations).Create(&apps).Error -} - -func (a AppRepo) GetByKey(ctx context.Context, key string) (model.App, error) { - var app model.App - if err := getTx(ctx).Where("key = ?", key).First(&app).Error; err != nil { - return app, err - } - return app, nil -} - -func (a AppRepo) Create(ctx context.Context, app *model.App) error { - return getTx(ctx).Omit(clause.Associations).Create(app).Error -} - -func (a AppRepo) Save(ctx context.Context, app *model.App) error { - return getTx(ctx).Omit(clause.Associations).Save(app).Error -} - -func (a AppRepo) BatchDelete(ctx context.Context, apps []model.App) error { - return getTx(ctx).Omit(clause.Associations).Delete(&apps).Error -} diff --git a/backend/app/repo/app_detail.go b/backend/app/repo/app_detail.go deleted file mode 100644 index c813ea5e9..000000000 --- a/backend/app/repo/app_detail.go +++ /dev/null @@ -1,82 +0,0 @@ -package repo - -import ( - "context" - "github.com/1Panel-dev/1Panel/backend/app/model" - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -type AppDetailRepo struct { -} - -type IAppDetailRepo interface { - WithVersion(version string) DBOption - WithAppId(id uint) DBOption - WithIgnored() DBOption - GetFirst(opts ...DBOption) (model.AppDetail, error) - Update(ctx context.Context, detail model.AppDetail) error - BatchCreate(ctx context.Context, details []model.AppDetail) error - DeleteByAppIds(ctx context.Context, appIds []uint) error - GetBy(opts ...DBOption) ([]model.AppDetail, error) - BatchUpdateBy(maps map[string]interface{}, opts ...DBOption) error - BatchDelete(ctx context.Context, appDetails []model.AppDetail) error -} - -func NewIAppDetailRepo() IAppDetailRepo { - return &AppDetailRepo{} -} - -func (a AppDetailRepo) WithVersion(version string) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("version = ?", version) - } -} - -func (a AppDetailRepo) WithAppId(id uint) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("app_id = ?", id) - } -} - -func (a AppDetailRepo) WithIgnored() DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("ignore_upgrade = 1") - } -} - -func (a AppDetailRepo) GetFirst(opts ...DBOption) (model.AppDetail, error) { - var detail model.AppDetail - err := getDb(opts...).Model(&model.AppDetail{}).Find(&detail).Error - return detail, err -} - -func (a AppDetailRepo) Update(ctx context.Context, detail model.AppDetail) error { - return getTx(ctx).Save(&detail).Error -} - -func (a AppDetailRepo) BatchCreate(ctx context.Context, details []model.AppDetail) error { - return getTx(ctx).Model(&model.AppDetail{}).Create(&details).Error -} - -func (a AppDetailRepo) DeleteByAppIds(ctx context.Context, appIds []uint) error { - return getTx(ctx).Where("app_id in (?)", appIds).Delete(&model.AppDetail{}).Error -} - -func (a AppDetailRepo) GetBy(opts ...DBOption) ([]model.AppDetail, error) { - var details []model.AppDetail - err := getDb(opts...).Find(&details).Error - return details, err -} - -func (a AppDetailRepo) BatchUpdateBy(maps map[string]interface{}, opts ...DBOption) error { - db := getDb(opts...).Model(&model.AppDetail{}) - if len(opts) == 0 { - db = db.Where("1=1") - } - return db.Updates(&maps).Error -} - -func (a AppDetailRepo) BatchDelete(ctx context.Context, appDetails []model.AppDetail) error { - return getTx(ctx).Omit(clause.Associations).Delete(&appDetails).Error -} diff --git a/backend/app/repo/app_install.go b/backend/app/repo/app_install.go deleted file mode 100644 index 7de128f4d..000000000 --- a/backend/app/repo/app_install.go +++ /dev/null @@ -1,241 +0,0 @@ -package repo - -import ( - "context" - "encoding/json" - - "github.com/1Panel-dev/1Panel/backend/constant" - - "gorm.io/gorm/clause" - - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" - "gorm.io/gorm" -) - -type AppInstallRepo struct{} - -type IAppInstallRepo interface { - WithDetailIdsIn(detailIds []uint) DBOption - WithDetailIdNotIn(detailIds []uint) DBOption - WithAppId(appId uint) DBOption - WithAppIdsIn(appIds []uint) DBOption - WithStatus(status string) DBOption - WithServiceName(serviceName string) DBOption - WithContainerName(containerName string) DBOption - WithPort(port int) DBOption - WithIdNotInWebsite() DBOption - WithIDNotIs(id uint) DBOption - ListBy(opts ...DBOption) ([]model.AppInstall, error) - GetFirst(opts ...DBOption) (model.AppInstall, error) - Create(ctx context.Context, install *model.AppInstall) error - Save(ctx context.Context, install *model.AppInstall) error - DeleteBy(opts ...DBOption) error - Delete(ctx context.Context, install model.AppInstall) error - Page(page, size int, opts ...DBOption) (int64, []model.AppInstall, error) - BatchUpdateBy(maps map[string]interface{}, opts ...DBOption) error - LoadBaseInfo(key string, name string) (*RootInfo, error) - GetFirstByCtx(ctx context.Context, opts ...DBOption) (model.AppInstall, error) -} - -func NewIAppInstallRepo() IAppInstallRepo { - return &AppInstallRepo{} -} - -func (a *AppInstallRepo) WithDetailIdsIn(detailIds []uint) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("app_detail_id in (?)", detailIds) - } -} - -func (a *AppInstallRepo) WithDetailIdNotIn(detailIds []uint) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("app_detail_id not in (?)", detailIds) - } -} - -func (a *AppInstallRepo) WithAppId(appId uint) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("app_id = ?", appId) - } -} - -func (a *AppInstallRepo) WithIDNotIs(id uint) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("id != ?", id) - } -} - -func (a *AppInstallRepo) WithAppIdsIn(appIds []uint) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("app_id in (?)", appIds) - } -} - -func (a *AppInstallRepo) WithStatus(status string) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("status = ?", status) - } -} - -func (a *AppInstallRepo) WithServiceName(serviceName string) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("service_name = ?", serviceName) - } -} - -func (a *AppInstallRepo) WithContainerName(containerName string) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("container_name = ?", containerName) - } -} - -func (a *AppInstallRepo) WithPort(port int) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("https_port = ? or http_port = ?", port, port) - } -} - -func (a *AppInstallRepo) WithIdNotInWebsite() DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("id not in (select app_install_id from websites)") - } -} - -func (a *AppInstallRepo) ListBy(opts ...DBOption) ([]model.AppInstall, error) { - var install []model.AppInstall - db := getDb(opts...).Model(&model.AppInstall{}) - err := db.Preload("App").Find(&install).Error - return install, err -} - -func (a *AppInstallRepo) GetFirst(opts ...DBOption) (model.AppInstall, error) { - var install model.AppInstall - db := getDb(opts...).Model(&model.AppInstall{}) - err := db.Preload("App").First(&install).Error - return install, err -} - -func (a *AppInstallRepo) GetFirstByCtx(ctx context.Context, opts ...DBOption) (model.AppInstall, error) { - var install model.AppInstall - db := getTx(ctx, opts...).Model(&model.AppInstall{}) - err := db.Preload("App").First(&install).Error - return install, err -} - -func (a *AppInstallRepo) Create(ctx context.Context, install *model.AppInstall) error { - db := getTx(ctx).Model(&model.AppInstall{}) - return db.Omit(clause.Associations).Create(&install).Error -} - -func (a *AppInstallRepo) Save(ctx context.Context, install *model.AppInstall) error { - return getTx(ctx).Save(&install).Error -} - -func (a *AppInstallRepo) DeleteBy(opts ...DBOption) error { - return getDb(opts...).Delete(&model.AppInstall{}).Error -} - -func (a *AppInstallRepo) Delete(ctx context.Context, install model.AppInstall) error { - db := getTx(ctx).Model(&model.AppInstall{}) - return db.Delete(&install).Error -} - -func (a *AppInstallRepo) Page(page, size int, opts ...DBOption) (int64, []model.AppInstall, error) { - var apps []model.AppInstall - db := getDb(opts...).Model(&model.AppInstall{}) - count := int64(0) - db = db.Count(&count) - err := db.Limit(size).Offset(size * (page - 1)).Preload("App").Find(&apps).Error - return count, apps, err -} - -func (a *AppInstallRepo) BatchUpdateBy(maps map[string]interface{}, opts ...DBOption) error { - db := getDb(opts...).Model(&model.AppInstall{}) - if len(opts) == 0 { - db = db.Where("1=1") - } - return db.Updates(&maps).Error -} - -type RootInfo struct { - ID uint `json:"id"` - Name string `json:"name"` - Status string `json:"status"` - Port int64 `json:"port"` - HttpsPort int64 `json:"httpsPort"` - UserName string `json:"userName"` - Password string `json:"password"` - UserPassword string `json:"userPassword"` - ContainerName string `json:"containerName"` - ServiceName string `json:"serviceName"` - Param string `json:"param"` - Env string `json:"env"` - Key string `json:"key"` - Version string `json:"version"` - AppPath string `json:"app_path"` -} - -func (a *AppInstallRepo) LoadBaseInfo(key string, name string) (*RootInfo, error) { - var ( - app model.App - appInstall model.AppInstall - info RootInfo - ) - if err := global.DB.Where("key = ?", key).First(&app).Error; err != nil { - return nil, err - } - if len(name) == 0 { - if err := global.DB.Where("app_id = ?", app.ID).First(&appInstall).Error; err != nil { - return nil, err - } - } else { - if err := global.DB.Where("app_id = ? AND name = ?", app.ID, name).First(&appInstall).Error; err != nil { - return nil, err - } - } - envMap := make(map[string]interface{}) - if err := json.Unmarshal([]byte(appInstall.Env), &envMap); err != nil { - return nil, err - } - switch app.Key { - case "mysql", "mariadb": - password, ok := envMap["PANEL_DB_ROOT_PASSWORD"].(string) - if ok { - info.Password = password - } - case "redis": - password, ok := envMap["PANEL_REDIS_ROOT_PASSWORD"].(string) - if ok { - info.Password = password - } - case "mongodb", constant.AppPostgresql: - user, ok := envMap["PANEL_DB_ROOT_USER"].(string) - if ok { - info.UserName = user - } - password, ok := envMap["PANEL_DB_ROOT_PASSWORD"].(string) - if ok { - info.Password = password - } - } - - userPassword, ok := envMap["PANEL_DB_USER_PASSWORD"].(string) - if ok { - info.UserPassword = userPassword - } - info.Port = int64(appInstall.HttpPort) - info.HttpsPort = int64(appInstall.HttpsPort) - info.ID = appInstall.ID - info.ServiceName = appInstall.ServiceName - info.ContainerName = appInstall.ContainerName - info.Name = appInstall.Name - info.Env = appInstall.Env - info.Param = appInstall.Param - info.Version = appInstall.Version - info.Key = app.Key - appInstall.App = app - info.AppPath = appInstall.GetAppPath() - info.Status = appInstall.Status - return &info, nil -} diff --git a/backend/app/repo/app_install_resource.go b/backend/app/repo/app_install_resource.go deleted file mode 100644 index e57bbf87d..000000000 --- a/backend/app/repo/app_install_resource.go +++ /dev/null @@ -1,82 +0,0 @@ -package repo - -import ( - "context" - - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" - "gorm.io/gorm" -) - -type AppInstallResourceRpo struct { -} - -type IAppInstallResourceRpo interface { - WithAppInstallId(appInstallId uint) DBOption - WithLinkId(linkId uint) DBOption - WithResourceId(resourceId uint) DBOption - GetBy(opts ...DBOption) ([]model.AppInstallResource, error) - GetFirst(opts ...DBOption) (model.AppInstallResource, error) - Create(ctx context.Context, resource *model.AppInstallResource) error - DeleteBy(ctx context.Context, opts ...DBOption) error - BatchUpdateBy(maps map[string]interface{}, opts ...DBOption) error -} - -func NewIAppInstallResourceRpo() IAppInstallResourceRpo { - return &AppInstallResourceRpo{} -} - -func (a AppInstallResourceRpo) WithAppInstallId(appInstallId uint) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("app_install_id = ?", appInstallId) - } -} - -func (a AppInstallResourceRpo) WithLinkId(linkId uint) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("link_id = ?", linkId) - } -} - -func (a AppInstallResourceRpo) WithResourceId(resourceId uint) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("resource_id = ?", resourceId) - } -} - -func (a AppInstallResourceRpo) GetBy(opts ...DBOption) ([]model.AppInstallResource, error) { - db := global.DB.Model(&model.AppInstallResource{}) - var resources []model.AppInstallResource - for _, opt := range opts { - db = opt(db) - } - err := db.Find(&resources).Error - return resources, err -} - -func (a AppInstallResourceRpo) GetFirst(opts ...DBOption) (model.AppInstallResource, error) { - db := global.DB.Model(&model.AppInstallResource{}) - var resources model.AppInstallResource - for _, opt := range opts { - db = opt(db) - } - err := db.First(&resources).Error - return resources, err -} - -func (a AppInstallResourceRpo) Create(ctx context.Context, resource *model.AppInstallResource) error { - db := getTx(ctx).Model(&model.AppInstallResource{}) - return db.Create(&resource).Error -} - -func (a AppInstallResourceRpo) DeleteBy(ctx context.Context, opts ...DBOption) error { - return getTx(ctx, opts...).Delete(&model.AppInstallResource{}).Error -} - -func (a *AppInstallResourceRpo) BatchUpdateBy(maps map[string]interface{}, opts ...DBOption) error { - db := getDb(opts...).Model(&model.AppInstallResource{}) - if len(opts) == 0 { - db = db.Where("1=1") - } - return db.Updates(&maps).Error -} diff --git a/backend/app/repo/app_tag.go b/backend/app/repo/app_tag.go deleted file mode 100644 index 0f65c91a3..000000000 --- a/backend/app/repo/app_tag.go +++ /dev/null @@ -1,49 +0,0 @@ -package repo - -import ( - "context" - "github.com/1Panel-dev/1Panel/backend/app/model" -) - -type AppTagRepo struct { -} - -type IAppTagRepo interface { - BatchCreate(ctx context.Context, tags []*model.AppTag) error - DeleteByAppIds(ctx context.Context, appIds []uint) error - DeleteAll(ctx context.Context) error - GetByAppId(appId uint) ([]model.AppTag, error) - GetByTagIds(tagIds []uint) ([]model.AppTag, error) -} - -func NewIAppTagRepo() IAppTagRepo { - return &AppTagRepo{} -} - -func (a AppTagRepo) BatchCreate(ctx context.Context, tags []*model.AppTag) error { - return getTx(ctx).Create(&tags).Error -} - -func (a AppTagRepo) DeleteByAppIds(ctx context.Context, appIds []uint) error { - return getTx(ctx).Where("app_id in (?)", appIds).Delete(&model.AppTag{}).Error -} - -func (a AppTagRepo) DeleteAll(ctx context.Context) error { - return getTx(ctx).Where("1 = 1").Delete(&model.AppTag{}).Error -} - -func (a AppTagRepo) GetByAppId(appId uint) ([]model.AppTag, error) { - var appTags []model.AppTag - if err := getDb().Where("app_id = ?", appId).Find(&appTags).Error; err != nil { - return nil, err - } - return appTags, nil -} - -func (a AppTagRepo) GetByTagIds(tagIds []uint) ([]model.AppTag, error) { - var appTags []model.AppTag - if err := getDb().Where("tag_id in (?)", tagIds).Find(&appTags).Error; err != nil { - return nil, err - } - return appTags, nil -} diff --git a/backend/app/repo/backup.go b/backend/app/repo/backup.go deleted file mode 100644 index 7548ba8ba..000000000 --- a/backend/app/repo/backup.go +++ /dev/null @@ -1,135 +0,0 @@ -package repo - -import ( - "context" - - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" - "gorm.io/gorm" -) - -type BackupRepo struct{} - -type IBackupRepo interface { - Get(opts ...DBOption) (model.BackupAccount, error) - ListRecord(opts ...DBOption) ([]model.BackupRecord, error) - PageRecord(page, size int, opts ...DBOption) (int64, []model.BackupRecord, error) - List(opts ...DBOption) ([]model.BackupAccount, error) - Create(backup *model.BackupAccount) error - CreateRecord(record *model.BackupRecord) error - Update(id uint, vars map[string]interface{}) error - Delete(opts ...DBOption) error - DeleteRecord(ctx context.Context, opts ...DBOption) error - UpdateRecord(record *model.BackupRecord) error - WithByDetailName(detailName string) DBOption - WithByFileName(fileName string) DBOption - WithByType(backupType string) DBOption - WithByCronID(cronjobID uint) DBOption -} - -func NewIBackupRepo() IBackupRepo { - return &BackupRepo{} -} - -func (u *BackupRepo) Get(opts ...DBOption) (model.BackupAccount, error) { - var backup model.BackupAccount - db := global.DB - for _, opt := range opts { - db = opt(db) - } - err := db.First(&backup).Error - return backup, err -} - -func (u *BackupRepo) ListRecord(opts ...DBOption) ([]model.BackupRecord, error) { - var users []model.BackupRecord - db := global.DB.Model(&model.BackupRecord{}) - for _, opt := range opts { - db = opt(db) - } - err := db.Find(&users).Error - return users, err -} - -func (u *BackupRepo) PageRecord(page, size int, opts ...DBOption) (int64, []model.BackupRecord, error) { - var users []model.BackupRecord - db := global.DB.Model(&model.BackupRecord{}) - for _, opt := range opts { - db = opt(db) - } - count := int64(0) - db = db.Count(&count) - err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error - return count, users, err -} - -func (u *BackupRepo) WithByDetailName(detailName string) DBOption { - return func(g *gorm.DB) *gorm.DB { - if len(detailName) == 0 { - return g - } - return g.Where("detail_name = ?", detailName) - } -} - -func (u *BackupRepo) WithByFileName(fileName string) DBOption { - return func(g *gorm.DB) *gorm.DB { - if len(fileName) == 0 { - return g - } - return g.Where("file_name = ?", fileName) - } -} - -func (u *BackupRepo) WithByType(backupType string) DBOption { - return func(g *gorm.DB) *gorm.DB { - if len(backupType) == 0 { - return g - } - return g.Where("type = ?", backupType) - } -} - -func (u *BackupRepo) List(opts ...DBOption) ([]model.BackupAccount, error) { - var ops []model.BackupAccount - db := global.DB.Model(&model.BackupAccount{}) - for _, opt := range opts { - db = opt(db) - } - err := db.Find(&ops).Error - return ops, err -} - -func (u *BackupRepo) Create(backup *model.BackupAccount) error { - return global.DB.Create(backup).Error -} - -func (u *BackupRepo) CreateRecord(record *model.BackupRecord) error { - return global.DB.Create(record).Error -} - -func (u *BackupRepo) UpdateRecord(record *model.BackupRecord) error { - return global.DB.Save(record).Error -} - -func (u *BackupRepo) Update(id uint, vars map[string]interface{}) error { - return global.DB.Model(&model.BackupAccount{}).Where("id = ?", id).Updates(vars).Error -} - -func (u *BackupRepo) Delete(opts ...DBOption) error { - db := global.DB - for _, opt := range opts { - db = opt(db) - } - return db.Delete(&model.BackupAccount{}).Error -} - -func (u *BackupRepo) DeleteRecord(ctx context.Context, opts ...DBOption) error { - return getTx(ctx, opts...).Delete(&model.BackupRecord{}).Error -} - -func (u *BackupRepo) WithByCronID(cronjobID uint) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("cronjob_id = ?", cronjobID) - } -} diff --git a/backend/app/repo/clam.go b/backend/app/repo/clam.go deleted file mode 100644 index 4fd212c8c..000000000 --- a/backend/app/repo/clam.go +++ /dev/null @@ -1,69 +0,0 @@ -package repo - -import ( - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" -) - -type ClamRepo struct{} - -type IClamRepo interface { - Page(limit, offset int, opts ...DBOption) (int64, []model.Clam, error) - Create(clam *model.Clam) error - Update(id uint, vars map[string]interface{}) error - Delete(opts ...DBOption) error - Get(opts ...DBOption) (model.Clam, error) - List(opts ...DBOption) ([]model.Clam, error) -} - -func NewIClamRepo() IClamRepo { - return &ClamRepo{} -} - -func (u *ClamRepo) Get(opts ...DBOption) (model.Clam, error) { - var clam model.Clam - db := global.DB - for _, opt := range opts { - db = opt(db) - } - err := db.First(&clam).Error - return clam, err -} - -func (u *ClamRepo) List(opts ...DBOption) ([]model.Clam, error) { - var clam []model.Clam - db := global.DB - for _, opt := range opts { - db = opt(db) - } - err := db.Find(&clam).Error - return clam, err -} - -func (u *ClamRepo) Page(page, size int, opts ...DBOption) (int64, []model.Clam, error) { - var users []model.Clam - db := global.DB.Model(&model.Clam{}) - for _, opt := range opts { - db = opt(db) - } - count := int64(0) - db = db.Count(&count) - err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error - return count, users, err -} - -func (u *ClamRepo) Create(clam *model.Clam) error { - return global.DB.Create(clam).Error -} - -func (u *ClamRepo) Update(id uint, vars map[string]interface{}) error { - return global.DB.Model(&model.Clam{}).Where("id = ?", id).Updates(vars).Error -} - -func (u *ClamRepo) Delete(opts ...DBOption) error { - db := global.DB - for _, opt := range opts { - db = opt(db) - } - return db.Delete(&model.Clam{}).Error -} diff --git a/backend/app/repo/command.go b/backend/app/repo/command.go deleted file mode 100644 index c68ad618a..000000000 --- a/backend/app/repo/command.go +++ /dev/null @@ -1,141 +0,0 @@ -package repo - -import ( - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" - "gorm.io/gorm" -) - -type CommandRepo struct{} - -type ICommandRepo interface { - GetList(opts ...DBOption) ([]model.Command, error) - Page(limit, offset int, opts ...DBOption) (int64, []model.Command, error) - WithByInfo(info string) DBOption - Create(command *model.Command) error - Update(id uint, vars map[string]interface{}) error - Delete(opts ...DBOption) error - Get(opts ...DBOption) (model.Command, error) - WithLikeName(name string) DBOption - - PageRedis(limit, offset int, opts ...DBOption) (int64, []model.RedisCommand, error) - GetRedis(opts ...DBOption) (model.RedisCommand, error) - GetRedisList(opts ...DBOption) ([]model.RedisCommand, error) - SaveRedis(command *model.RedisCommand) error - DeleteRedis(opts ...DBOption) error -} - -func NewICommandRepo() ICommandRepo { - return &CommandRepo{} -} - -func (u *CommandRepo) Get(opts ...DBOption) (model.Command, error) { - var command model.Command - db := global.DB - for _, opt := range opts { - db = opt(db) - } - err := db.First(&command).Error - return command, err -} - -func (u *CommandRepo) GetRedis(opts ...DBOption) (model.RedisCommand, error) { - var command model.RedisCommand - db := global.DB - for _, opt := range opts { - db = opt(db) - } - err := db.First(&command).Error - return command, err -} - -func (u *CommandRepo) Page(page, size int, opts ...DBOption) (int64, []model.Command, error) { - var users []model.Command - db := global.DB.Model(&model.Command{}) - for _, opt := range opts { - db = opt(db) - } - count := int64(0) - db = db.Count(&count) - err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error - return count, users, err -} - -func (u *CommandRepo) PageRedis(page, size int, opts ...DBOption) (int64, []model.RedisCommand, error) { - var users []model.RedisCommand - db := global.DB.Model(&model.RedisCommand{}) - for _, opt := range opts { - db = opt(db) - } - count := int64(0) - db = db.Count(&count) - err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error - return count, users, err -} - -func (u *CommandRepo) GetList(opts ...DBOption) ([]model.Command, error) { - var commands []model.Command - db := global.DB.Model(&model.Command{}) - for _, opt := range opts { - db = opt(db) - } - err := db.Find(&commands).Error - return commands, err -} - -func (u *CommandRepo) GetRedisList(opts ...DBOption) ([]model.RedisCommand, error) { - var commands []model.RedisCommand - db := global.DB.Model(&model.RedisCommand{}) - for _, opt := range opts { - db = opt(db) - } - err := db.Find(&commands).Error - return commands, err -} - -func (c *CommandRepo) WithByInfo(info string) DBOption { - return func(g *gorm.DB) *gorm.DB { - if len(info) == 0 { - return g - } - infoStr := "%" + info + "%" - return g.Where("name LIKE ? OR addr LIKE ?", infoStr, infoStr) - } -} - -func (u *CommandRepo) Create(command *model.Command) error { - return global.DB.Create(command).Error -} - -func (u *CommandRepo) SaveRedis(command *model.RedisCommand) error { - return global.DB.Save(command).Error -} - -func (u *CommandRepo) Update(id uint, vars map[string]interface{}) error { - return global.DB.Model(&model.Command{}).Where("id = ?", id).Updates(vars).Error -} - -func (u *CommandRepo) Delete(opts ...DBOption) error { - db := global.DB - for _, opt := range opts { - db = opt(db) - } - return db.Delete(&model.Command{}).Error -} - -func (u *CommandRepo) DeleteRedis(opts ...DBOption) error { - db := global.DB - for _, opt := range opts { - db = opt(db) - } - return db.Delete(&model.RedisCommand{}).Error -} - -func (a CommandRepo) WithLikeName(name string) DBOption { - return func(g *gorm.DB) *gorm.DB { - if len(name) == 0 { - return g - } - return g.Where("name like ? or command like ?", "%"+name+"%", "%"+name+"%") - } -} diff --git a/backend/app/repo/common.go b/backend/app/repo/common.go deleted file mode 100644 index e6cf0b23f..000000000 --- a/backend/app/repo/common.go +++ /dev/null @@ -1,149 +0,0 @@ -package repo - -import ( - "context" - "fmt" - "time" - - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "gorm.io/gorm" -) - -type DBOption func(*gorm.DB) *gorm.DB - -type ICommonRepo interface { - WithByID(id uint) DBOption - WithByName(name string) DBOption - WithByType(tp string) DBOption - WithOrderBy(orderStr string) DBOption - WithOrderRuleBy(orderBy, order string) DBOption - WithByGroupID(groupID uint) DBOption - WithLikeName(name string) DBOption - WithIdsIn(ids []uint) DBOption - WithByDate(startTime, endTime time.Time) DBOption - WithByStartDate(startTime time.Time) DBOption - WithByStatus(status string) DBOption - WithByFrom(from string) DBOption -} - -type CommonRepo struct{} - -func NewCommonRepo() ICommonRepo { - return &CommonRepo{} -} - -func (c *CommonRepo) WithByID(id uint) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("id = ?", id) - } -} - -func (c *CommonRepo) WithByName(name string) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("name = ?", name) - } -} - -func (c *CommonRepo) WithByDate(startTime, endTime time.Time) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("start_time > ? AND start_time < ?", startTime, endTime) - } -} - -func (c *CommonRepo) WithByStartDate(startTime time.Time) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("start_time < ?", startTime) - } -} - -func (c *CommonRepo) WithByType(tp string) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("type = ?", tp) - } -} - -func (c *CommonRepo) WithByGroupID(groupID uint) DBOption { - return func(g *gorm.DB) *gorm.DB { - if groupID == 0 { - return g - } - return g.Where("group_id = ?", groupID) - } -} - -func (c *CommonRepo) WithByStatus(status string) DBOption { - return func(g *gorm.DB) *gorm.DB { - if len(status) == 0 { - return g - } - return g.Where("status = ?", status) - } -} - -func (c *CommonRepo) WithByFrom(from string) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("`from` = ?", from) - } -} - -func (c *CommonRepo) WithLikeName(name string) DBOption { - return func(g *gorm.DB) *gorm.DB { - if len(name) == 0 { - return g - } - return g.Where("name like ?", "%"+name+"%") - } -} - -func (c *CommonRepo) WithOrderBy(orderStr string) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Order(orderStr) - } -} - -func (c *CommonRepo) WithOrderRuleBy(orderBy, order string) DBOption { - switch order { - case constant.OrderDesc: - order = "desc" - case constant.OrderAsc: - order = "asc" - default: - orderBy = "created_at" - order = "desc" - } - return func(g *gorm.DB) *gorm.DB { - return g.Order(fmt.Sprintf("%s %s", orderBy, order)) - } -} - -func (c *CommonRepo) WithIdsIn(ids []uint) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("id in (?)", ids) - } -} - -func (c *CommonRepo) WithIdsNotIn(ids []uint) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("id not in (?)", ids) - } -} - -func getTx(ctx context.Context, opts ...DBOption) *gorm.DB { - tx, ok := ctx.Value(constant.DB).(*gorm.DB) - if ok { - for _, opt := range opts { - tx = opt(tx) - } - return tx - } - return getDb(opts...) -} - -func getDb(opts ...DBOption) *gorm.DB { - db := global.DB - for _, opt := range opts { - db = opt(db) - } - return db -} diff --git a/backend/app/repo/compose_template.go b/backend/app/repo/compose_template.go deleted file mode 100644 index 4259e2fb5..000000000 --- a/backend/app/repo/compose_template.go +++ /dev/null @@ -1,104 +0,0 @@ -package repo - -import ( - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" -) - -type ComposeTemplateRepo struct{} - -type IComposeTemplateRepo interface { - Get(opts ...DBOption) (model.ComposeTemplate, error) - List(opts ...DBOption) ([]model.ComposeTemplate, error) - Page(limit, offset int, opts ...DBOption) (int64, []model.ComposeTemplate, error) - Create(compose *model.ComposeTemplate) error - Update(id uint, vars map[string]interface{}) error - Delete(opts ...DBOption) error - - GetRecord(opts ...DBOption) (model.Compose, error) - CreateRecord(compose *model.Compose) error - DeleteRecord(opts ...DBOption) error - ListRecord() ([]model.Compose, error) -} - -func NewIComposeTemplateRepo() IComposeTemplateRepo { - return &ComposeTemplateRepo{} -} - -func (u *ComposeTemplateRepo) Get(opts ...DBOption) (model.ComposeTemplate, error) { - var compose model.ComposeTemplate - db := global.DB - for _, opt := range opts { - db = opt(db) - } - err := db.First(&compose).Error - return compose, err -} - -func (u *ComposeTemplateRepo) Page(page, size int, opts ...DBOption) (int64, []model.ComposeTemplate, error) { - var users []model.ComposeTemplate - db := global.DB.Model(&model.ComposeTemplate{}) - for _, opt := range opts { - db = opt(db) - } - count := int64(0) - db = db.Count(&count) - err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error - return count, users, err -} - -func (u *ComposeTemplateRepo) List(opts ...DBOption) ([]model.ComposeTemplate, error) { - var composes []model.ComposeTemplate - db := global.DB.Model(&model.ComposeTemplate{}) - for _, opt := range opts { - db = opt(db) - } - err := db.Find(&composes).Error - return composes, err -} - -func (u *ComposeTemplateRepo) Create(compose *model.ComposeTemplate) error { - return global.DB.Create(compose).Error -} - -func (u *ComposeTemplateRepo) Update(id uint, vars map[string]interface{}) error { - return global.DB.Model(&model.ComposeTemplate{}).Where("id = ?", id).Updates(vars).Error -} - -func (u *ComposeTemplateRepo) Delete(opts ...DBOption) error { - db := global.DB - for _, opt := range opts { - db = opt(db) - } - return db.Delete(&model.ComposeTemplate{}).Error -} - -func (u *ComposeTemplateRepo) GetRecord(opts ...DBOption) (model.Compose, error) { - var compose model.Compose - db := global.DB - for _, opt := range opts { - db = opt(db) - } - err := db.First(&compose).Error - return compose, err -} - -func (u *ComposeTemplateRepo) ListRecord() ([]model.Compose, error) { - var composes []model.Compose - if err := global.DB.Find(&composes).Error; err != nil { - return nil, err - } - return composes, nil -} - -func (u *ComposeTemplateRepo) CreateRecord(compose *model.Compose) error { - return global.DB.Create(compose).Error -} - -func (u *ComposeTemplateRepo) DeleteRecord(opts ...DBOption) error { - db := global.DB - for _, opt := range opts { - db = opt(db) - } - return db.Delete(&model.Compose{}).Error -} diff --git a/backend/app/repo/cronjob.go b/backend/app/repo/cronjob.go deleted file mode 100644 index 867fc6283..000000000 --- a/backend/app/repo/cronjob.go +++ /dev/null @@ -1,192 +0,0 @@ -package repo - -import ( - "time" - - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "gorm.io/gorm" -) - -type CronjobRepo struct{} - -type ICronjobRepo interface { - Get(opts ...DBOption) (model.Cronjob, error) - GetRecord(opts ...DBOption) (model.JobRecords, error) - RecordFirst(id uint) (model.JobRecords, error) - ListRecord(opts ...DBOption) ([]model.JobRecords, error) - List(opts ...DBOption) ([]model.Cronjob, error) - Page(limit, offset int, opts ...DBOption) (int64, []model.Cronjob, error) - Create(cronjob *model.Cronjob) error - WithByJobID(id int) DBOption - WithByDbName(name string) DBOption - WithByDefaultDownload(account string) DBOption - WithByRecordDropID(id int) DBOption - WithByRecordFile(file string) DBOption - Save(id uint, cronjob model.Cronjob) error - Update(id uint, vars map[string]interface{}) error - Delete(opts ...DBOption) error - DeleteRecord(opts ...DBOption) error - StartRecords(cronjobID uint, fromLocal bool, targetPath string) model.JobRecords - UpdateRecords(id uint, vars map[string]interface{}) error - EndRecords(record model.JobRecords, status, message, records string) - PageRecords(page, size int, opts ...DBOption) (int64, []model.JobRecords, error) -} - -func NewICronjobRepo() ICronjobRepo { - return &CronjobRepo{} -} - -func (u *CronjobRepo) Get(opts ...DBOption) (model.Cronjob, error) { - var cronjob model.Cronjob - db := global.DB - for _, opt := range opts { - db = opt(db) - } - err := db.First(&cronjob).Error - return cronjob, err -} - -func (u *CronjobRepo) GetRecord(opts ...DBOption) (model.JobRecords, error) { - var record model.JobRecords - db := global.DB - for _, opt := range opts { - db = opt(db) - } - err := db.First(&record).Error - return record, err -} - -func (u *CronjobRepo) List(opts ...DBOption) ([]model.Cronjob, error) { - var cronjobs []model.Cronjob - db := global.DB.Model(&model.Cronjob{}) - for _, opt := range opts { - db = opt(db) - } - err := db.Find(&cronjobs).Error - return cronjobs, err -} - -func (u *CronjobRepo) ListRecord(opts ...DBOption) ([]model.JobRecords, error) { - var cronjobs []model.JobRecords - db := global.DB.Model(&model.JobRecords{}) - for _, opt := range opts { - db = opt(db) - } - err := db.Find(&cronjobs).Error - return cronjobs, err -} - -func (u *CronjobRepo) Page(page, size int, opts ...DBOption) (int64, []model.Cronjob, error) { - var cronjobs []model.Cronjob - db := global.DB.Model(&model.Cronjob{}) - for _, opt := range opts { - db = opt(db) - } - count := int64(0) - db = db.Count(&count) - err := db.Limit(size).Offset(size * (page - 1)).Find(&cronjobs).Error - return count, cronjobs, err -} - -func (u *CronjobRepo) RecordFirst(id uint) (model.JobRecords, error) { - var record model.JobRecords - err := global.DB.Where("cronjob_id = ?", id).Order("created_at desc").First(&record).Error - return record, err -} - -func (u *CronjobRepo) PageRecords(page, size int, opts ...DBOption) (int64, []model.JobRecords, error) { - var cronjobs []model.JobRecords - db := global.DB.Model(&model.JobRecords{}) - for _, opt := range opts { - db = opt(db) - } - count := int64(0) - db = db.Count(&count) - err := db.Order("created_at desc").Limit(size).Offset(size * (page - 1)).Find(&cronjobs).Error - return count, cronjobs, err -} - -func (u *CronjobRepo) Create(cronjob *model.Cronjob) error { - return global.DB.Create(cronjob).Error -} - -func (c *CronjobRepo) WithByJobID(id int) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("cronjob_id = ?", id) - } -} - -func (c *CronjobRepo) WithByDbName(name string) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("db_name = ?", name) - } -} - -func (c *CronjobRepo) WithByDefaultDownload(account string) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("default_download = ?", account) - } -} - -func (c *CronjobRepo) WithByRecordFile(file string) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("records = ?", file) - } -} - -func (c *CronjobRepo) WithByRecordDropID(id int) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("id < ?", id) - } -} - -func (u *CronjobRepo) StartRecords(cronjobID uint, fromLocal bool, targetPath string) model.JobRecords { - var record model.JobRecords - record.StartTime = time.Now() - record.CronjobID = cronjobID - record.FromLocal = fromLocal - record.Status = constant.StatusWaiting - if err := global.DB.Create(&record).Error; err != nil { - global.LOG.Errorf("create record status failed, err: %v", err) - } - return record -} -func (u *CronjobRepo) EndRecords(record model.JobRecords, status, message, records string) { - errMap := make(map[string]interface{}) - errMap["records"] = records - errMap["status"] = status - errMap["file"] = record.File - errMap["message"] = message - errMap["interval"] = time.Since(record.StartTime).Milliseconds() - if err := global.DB.Model(&model.JobRecords{}).Where("id = ?", record.ID).Updates(errMap).Error; err != nil { - global.LOG.Errorf("update record status failed, err: %v", err) - } -} - -func (u *CronjobRepo) Save(id uint, cronjob model.Cronjob) error { - return global.DB.Model(&model.Cronjob{}).Where("id = ?", id).Save(&cronjob).Error -} -func (u *CronjobRepo) Update(id uint, vars map[string]interface{}) error { - return global.DB.Model(&model.Cronjob{}).Where("id = ?", id).Updates(vars).Error -} - -func (u *CronjobRepo) UpdateRecords(id uint, vars map[string]interface{}) error { - return global.DB.Model(&model.JobRecords{}).Where("id = ?", id).Updates(vars).Error -} - -func (u *CronjobRepo) Delete(opts ...DBOption) error { - db := global.DB - for _, opt := range opts { - db = opt(db) - } - return db.Delete(&model.Cronjob{}).Error -} -func (u *CronjobRepo) DeleteRecord(opts ...DBOption) error { - db := global.DB - for _, opt := range opts { - db = opt(db) - } - return db.Delete(&model.JobRecords{}).Error -} diff --git a/backend/app/repo/database.go b/backend/app/repo/database.go deleted file mode 100644 index bd7749532..000000000 --- a/backend/app/repo/database.go +++ /dev/null @@ -1,143 +0,0 @@ -package repo - -import ( - "context" - "fmt" - "strings" - - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/encrypt" - "gorm.io/gorm" -) - -type DatabaseRepo struct{} - -type IDatabaseRepo interface { - GetList(opts ...DBOption) ([]model.Database, error) - Page(limit, offset int, opts ...DBOption) (int64, []model.Database, error) - Create(ctx context.Context, database *model.Database) error - Update(id uint, vars map[string]interface{}) error - Delete(ctx context.Context, opts ...DBOption) error - Get(opts ...DBOption) (model.Database, error) - WithByFrom(from string) DBOption - WithoutByFrom(from string) DBOption - WithAppInstallID(appInstallID uint) DBOption - WithTypeList(dbType string) DBOption -} - -func NewIDatabaseRepo() IDatabaseRepo { - return &DatabaseRepo{} -} - -func (d *DatabaseRepo) Get(opts ...DBOption) (model.Database, error) { - var database model.Database - db := global.DB - for _, opt := range opts { - db = opt(db) - } - if err := db.First(&database).Error; err != nil { - return database, err - } - pass, err := encrypt.StringDecrypt(database.Password) - if err != nil { - global.LOG.Errorf("decrypt database %s password failed, err: %v", database.Name, err) - } - database.Password = pass - return database, nil -} - -func (d *DatabaseRepo) Page(page, size int, opts ...DBOption) (int64, []model.Database, error) { - var databases []model.Database - db := global.DB.Model(&model.Database{}) - for _, opt := range opts { - db = opt(db) - } - count := int64(0) - db = db.Count(&count) - if err := db.Limit(size).Offset(size * (page - 1)).Find(&databases).Error; err != nil { - return count, databases, err - } - for i := 0; i < len(databases); i++ { - pass, err := encrypt.StringDecrypt(databases[i].Password) - if err != nil { - global.LOG.Errorf("decrypt database db %s password failed, err: %v", databases[i].Name, err) - } - databases[i].Password = pass - } - return count, databases, nil -} - -func (d *DatabaseRepo) GetList(opts ...DBOption) ([]model.Database, error) { - var databases []model.Database - db := global.DB.Model(&model.Database{}) - for _, opt := range opts { - db = opt(db) - } - if err := db.Find(&databases).Error; err != nil { - return databases, err - } - for i := 0; i < len(databases); i++ { - pass, err := encrypt.StringDecrypt(databases[i].Password) - if err != nil { - global.LOG.Errorf("decrypt database db %s password failed, err: %v", databases[i].Name, err) - } - databases[i].Password = pass - } - return databases, nil -} - -func (d *DatabaseRepo) WithByFrom(from string) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("`from` = ?", from) - } -} - -func (d *DatabaseRepo) WithoutByFrom(from string) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("`from` != ?", from) - } -} - -func (d *DatabaseRepo) WithTypeList(dbType string) DBOption { - return func(g *gorm.DB) *gorm.DB { - if !strings.Contains(dbType, ",") { - return g.Where("`type` = ?", dbType) - } - types := strings.Split(dbType, ",") - var ( - rules []string - values []interface{} - ) - for _, ty := range types { - if len(ty) != 0 { - rules = append(rules, "`type` = ?") - values = append(values, ty) - } - } - return g.Where(strings.Join(rules, " OR "), values...) - } -} - -func (d *DatabaseRepo) WithAppInstallID(appInstallID uint) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("app_install_id = ?", appInstallID) - } -} - -func (d *DatabaseRepo) Create(ctx context.Context, database *model.Database) error { - pass, err := encrypt.StringEncrypt(database.Password) - if err != nil { - return fmt.Errorf("decrypt database db %s password failed, err: %v", database.Name, err) - } - database.Password = pass - return getTx(ctx).Create(database).Error -} - -func (d *DatabaseRepo) Update(id uint, vars map[string]interface{}) error { - return global.DB.Model(&model.Database{}).Where("id = ?", id).Updates(vars).Error -} - -func (d *DatabaseRepo) Delete(ctx context.Context, opts ...DBOption) error { - return getTx(ctx, opts...).Delete(&model.Database{}).Error -} diff --git a/backend/app/repo/database_mysql.go b/backend/app/repo/database_mysql.go deleted file mode 100644 index 47d107379..000000000 --- a/backend/app/repo/database_mysql.go +++ /dev/null @@ -1,123 +0,0 @@ -package repo - -import ( - "context" - "fmt" - - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/encrypt" - "gorm.io/gorm" -) - -type MysqlRepo struct{} - -type IMysqlRepo interface { - Get(opts ...DBOption) (model.DatabaseMysql, error) - WithByMysqlName(mysqlName string) DBOption - WithByFrom(from string) DBOption - List(opts ...DBOption) ([]model.DatabaseMysql, error) - Page(limit, offset int, opts ...DBOption) (int64, []model.DatabaseMysql, error) - Create(ctx context.Context, mysql *model.DatabaseMysql) error - Delete(ctx context.Context, opts ...DBOption) error - Update(id uint, vars map[string]interface{}) error - DeleteLocal(ctx context.Context) error -} - -func NewIMysqlRepo() IMysqlRepo { - return &MysqlRepo{} -} - -func (u *MysqlRepo) Get(opts ...DBOption) (model.DatabaseMysql, error) { - var mysql model.DatabaseMysql - db := global.DB - for _, opt := range opts { - db = opt(db) - } - if err := db.First(&mysql).Error; err != nil { - return mysql, err - } - - pass, err := encrypt.StringDecrypt(mysql.Password) - if err != nil { - global.LOG.Errorf("decrypt database db %s password failed, err: %v", mysql.Name, err) - } - mysql.Password = pass - return mysql, err -} - -func (u *MysqlRepo) List(opts ...DBOption) ([]model.DatabaseMysql, error) { - var mysqls []model.DatabaseMysql - db := global.DB.Model(&model.DatabaseMysql{}) - for _, opt := range opts { - db = opt(db) - } - if err := db.Find(&mysqls).Error; err != nil { - return mysqls, err - } - for i := 0; i < len(mysqls); i++ { - pass, err := encrypt.StringDecrypt(mysqls[i].Password) - if err != nil { - global.LOG.Errorf("decrypt database db %s password failed, err: %v", mysqls[i].Name, err) - } - mysqls[i].Password = pass - } - return mysqls, nil -} - -func (u *MysqlRepo) Page(page, size int, opts ...DBOption) (int64, []model.DatabaseMysql, error) { - var mysqls []model.DatabaseMysql - db := global.DB.Model(&model.DatabaseMysql{}) - for _, opt := range opts { - db = opt(db) - } - count := int64(0) - db = db.Count(&count) - if err := db.Limit(size).Offset(size * (page - 1)).Find(&mysqls).Error; err != nil { - return count, mysqls, err - } - for i := 0; i < len(mysqls); i++ { - pass, err := encrypt.StringDecrypt(mysqls[i].Password) - if err != nil { - global.LOG.Errorf("decrypt database db %s password failed, err: %v", mysqls[i].Name, err) - } - mysqls[i].Password = pass - } - return count, mysqls, nil -} - -func (u *MysqlRepo) Create(ctx context.Context, mysql *model.DatabaseMysql) error { - pass, err := encrypt.StringEncrypt(mysql.Password) - if err != nil { - return fmt.Errorf("decrypt database db %s password failed, err: %v", mysql.Name, err) - } - mysql.Password = pass - return getTx(ctx).Create(mysql).Error -} - -func (u *MysqlRepo) Delete(ctx context.Context, opts ...DBOption) error { - return getTx(ctx, opts...).Delete(&model.DatabaseMysql{}).Error -} - -func (u *MysqlRepo) DeleteLocal(ctx context.Context) error { - return getTx(ctx).Where("`from` = ?", "local").Delete(&model.DatabaseMysql{}).Error -} - -func (u *MysqlRepo) Update(id uint, vars map[string]interface{}) error { - return global.DB.Model(&model.DatabaseMysql{}).Where("id = ?", id).Updates(vars).Error -} - -func (u *MysqlRepo) WithByMysqlName(mysqlName string) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("mysql_name = ?", mysqlName) - } -} - -func (u *MysqlRepo) WithByFrom(from string) DBOption { - return func(g *gorm.DB) *gorm.DB { - if len(from) != 0 { - return g.Where("`from` = ?", from) - } - return g - } -} diff --git a/backend/app/repo/database_postgresql.go b/backend/app/repo/database_postgresql.go deleted file mode 100644 index 23a89eb4c..000000000 --- a/backend/app/repo/database_postgresql.go +++ /dev/null @@ -1,123 +0,0 @@ -package repo - -import ( - "context" - "fmt" - - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/encrypt" - "gorm.io/gorm" -) - -type PostgresqlRepo struct{} - -type IPostgresqlRepo interface { - Get(opts ...DBOption) (model.DatabasePostgresql, error) - WithByPostgresqlName(postgresqlName string) DBOption - WithByFrom(from string) DBOption - List(opts ...DBOption) ([]model.DatabasePostgresql, error) - Page(limit, offset int, opts ...DBOption) (int64, []model.DatabasePostgresql, error) - Create(ctx context.Context, postgresql *model.DatabasePostgresql) error - Delete(ctx context.Context, opts ...DBOption) error - Update(id uint, vars map[string]interface{}) error - DeleteLocal(ctx context.Context) error -} - -func NewIPostgresqlRepo() IPostgresqlRepo { - return &PostgresqlRepo{} -} - -func (u *PostgresqlRepo) Get(opts ...DBOption) (model.DatabasePostgresql, error) { - var postgresql model.DatabasePostgresql - db := global.DB - for _, opt := range opts { - db = opt(db) - } - if err := db.First(&postgresql).Error; err != nil { - return postgresql, err - } - - pass, err := encrypt.StringDecrypt(postgresql.Password) - if err != nil { - global.LOG.Errorf("decrypt database db %s password failed, err: %v", postgresql.Name, err) - } - postgresql.Password = pass - return postgresql, err -} - -func (u *PostgresqlRepo) List(opts ...DBOption) ([]model.DatabasePostgresql, error) { - var postgresqls []model.DatabasePostgresql - db := global.DB.Model(&model.DatabasePostgresql{}) - for _, opt := range opts { - db = opt(db) - } - if err := db.Find(&postgresqls).Error; err != nil { - return postgresqls, err - } - for i := 0; i < len(postgresqls); i++ { - pass, err := encrypt.StringDecrypt(postgresqls[i].Password) - if err != nil { - global.LOG.Errorf("decrypt database db %s password failed, err: %v", postgresqls[i].Name, err) - } - postgresqls[i].Password = pass - } - return postgresqls, nil -} - -func (u *PostgresqlRepo) Page(page, size int, opts ...DBOption) (int64, []model.DatabasePostgresql, error) { - var postgresqls []model.DatabasePostgresql - db := global.DB.Model(&model.DatabasePostgresql{}) - for _, opt := range opts { - db = opt(db) - } - count := int64(0) - db = db.Count(&count) - if err := db.Limit(size).Offset(size * (page - 1)).Find(&postgresqls).Error; err != nil { - return count, postgresqls, err - } - for i := 0; i < len(postgresqls); i++ { - pass, err := encrypt.StringDecrypt(postgresqls[i].Password) - if err != nil { - global.LOG.Errorf("decrypt database db %s password failed, err: %v", postgresqls[i].Name, err) - } - postgresqls[i].Password = pass - } - return count, postgresqls, nil -} - -func (u *PostgresqlRepo) Create(ctx context.Context, postgresql *model.DatabasePostgresql) error { - pass, err := encrypt.StringEncrypt(postgresql.Password) - if err != nil { - return fmt.Errorf("decrypt database db %s password failed, err: %v", postgresql.Name, err) - } - postgresql.Password = pass - return getTx(ctx).Create(postgresql).Error -} - -func (u *PostgresqlRepo) Delete(ctx context.Context, opts ...DBOption) error { - return getTx(ctx, opts...).Delete(&model.DatabasePostgresql{}).Error -} - -func (u *PostgresqlRepo) DeleteLocal(ctx context.Context) error { - return getTx(ctx).Where("`from` = ?", "local").Delete(&model.DatabasePostgresql{}).Error -} - -func (u *PostgresqlRepo) Update(id uint, vars map[string]interface{}) error { - return global.DB.Model(&model.DatabasePostgresql{}).Where("id = ?", id).Updates(vars).Error -} - -func (u *PostgresqlRepo) WithByPostgresqlName(postgresqlName string) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("postgresql_name = ?", postgresqlName) - } -} - -func (u *PostgresqlRepo) WithByFrom(from string) DBOption { - return func(g *gorm.DB) *gorm.DB { - if len(from) != 0 { - return g.Where("`from` = ?", from) - } - return g - } -} diff --git a/backend/app/repo/favorite.go b/backend/app/repo/favorite.go deleted file mode 100644 index 01d2ba1ac..000000000 --- a/backend/app/repo/favorite.go +++ /dev/null @@ -1,66 +0,0 @@ -package repo - -import ( - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" - "gorm.io/gorm" -) - -type FavoriteRepo struct{} - -type IFavoriteRepo interface { - Page(page, size int, opts ...DBOption) (int64, []model.Favorite, error) - Create(group *model.Favorite) error - Delete(opts ...DBOption) error - GetFirst(opts ...DBOption) (model.Favorite, error) - All() ([]model.Favorite, error) - WithByPath(path string) DBOption -} - -func NewIFavoriteRepo() IFavoriteRepo { - return &FavoriteRepo{} -} - -func (f *FavoriteRepo) WithByPath(path string) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("path = ?", path) - } -} - -func (f *FavoriteRepo) Page(page, size int, opts ...DBOption) (int64, []model.Favorite, error) { - var ( - favorites []model.Favorite - count int64 - ) - count = int64(0) - db := getDb(opts...).Model(&model.Favorite{}) - db = db.Count(&count) - err := db.Limit(size).Offset(size * (page - 1)).Find(&favorites).Error - return count, favorites, err -} - -func (f *FavoriteRepo) Create(favorite *model.Favorite) error { - return global.DB.Create(favorite).Error -} - -func (f *FavoriteRepo) GetFirst(opts ...DBOption) (model.Favorite, error) { - var favorite model.Favorite - db := getDb(opts...).Model(&model.Favorite{}) - if err := db.First(&favorite).Error; err != nil { - return favorite, err - } - return favorite, nil -} - -func (f *FavoriteRepo) Delete(opts ...DBOption) error { - db := getDb(opts...).Model(&model.Favorite{}) - return db.Delete(&model.Favorite{}).Error -} - -func (f *FavoriteRepo) All() ([]model.Favorite, error) { - var favorites []model.Favorite - if err := getDb().Find(&favorites).Error; err != nil { - return nil, err - } - return favorites, nil -} diff --git a/backend/app/repo/ftp.go b/backend/app/repo/ftp.go deleted file mode 100644 index c9732ec13..000000000 --- a/backend/app/repo/ftp.go +++ /dev/null @@ -1,80 +0,0 @@ -package repo - -import ( - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" - "gorm.io/gorm" -) - -type FtpRepo struct{} - -type IFtpRepo interface { - Get(opts ...DBOption) (model.Ftp, error) - GetList(opts ...DBOption) ([]model.Ftp, error) - Page(limit, offset int, opts ...DBOption) (int64, []model.Ftp, error) - Create(ftp *model.Ftp) error - Update(id uint, vars map[string]interface{}) error - Delete(opts ...DBOption) error - WithByUser(user string) DBOption -} - -func NewIFtpRepo() IFtpRepo { - return &FtpRepo{} -} - -func (u *FtpRepo) Get(opts ...DBOption) (model.Ftp, error) { - var ftp model.Ftp - db := global.DB - for _, opt := range opts { - db = opt(db) - } - err := db.First(&ftp).Error - return ftp, err -} - -func (h *FtpRepo) WithByUser(user string) DBOption { - return func(g *gorm.DB) *gorm.DB { - if len(user) == 0 { - return g - } - return g.Where("user like ?", "%"+user+"%") - } -} - -func (u *FtpRepo) GetList(opts ...DBOption) ([]model.Ftp, error) { - var ftps []model.Ftp - db := global.DB - for _, opt := range opts { - db = opt(db) - } - err := db.Find(&ftps).Error - return ftps, err -} - -func (h *FtpRepo) Page(page, size int, opts ...DBOption) (int64, []model.Ftp, error) { - var users []model.Ftp - db := global.DB.Model(&model.Ftp{}) - for _, opt := range opts { - db = opt(db) - } - count := int64(0) - db = db.Count(&count) - err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error - return count, users, err -} - -func (h *FtpRepo) Create(ftp *model.Ftp) error { - return global.DB.Create(ftp).Error -} - -func (h *FtpRepo) Update(id uint, vars map[string]interface{}) error { - return global.DB.Model(&model.Ftp{}).Where("id = ?", id).Updates(vars).Error -} - -func (h *FtpRepo) Delete(opts ...DBOption) error { - db := global.DB - for _, opt := range opts { - db = opt(db) - } - return db.Delete(&model.Ftp{}).Error -} diff --git a/backend/app/repo/group.go b/backend/app/repo/group.go deleted file mode 100644 index 5384e188d..000000000 --- a/backend/app/repo/group.go +++ /dev/null @@ -1,69 +0,0 @@ -package repo - -import ( - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" - "gorm.io/gorm" -) - -type GroupRepo struct{} - -type IGroupRepo interface { - Get(opts ...DBOption) (model.Group, error) - GetList(opts ...DBOption) ([]model.Group, error) - Create(group *model.Group) error - Update(id uint, vars map[string]interface{}) error - Delete(opts ...DBOption) error - CancelDefault(groupType string) error - WithByHostDefault() DBOption -} - -func NewIGroupRepo() IGroupRepo { - return &GroupRepo{} -} - -func (u *GroupRepo) Get(opts ...DBOption) (model.Group, error) { - var group model.Group - db := global.DB - for _, opt := range opts { - db = opt(db) - } - err := db.First(&group).Error - return group, err -} - -func (u *GroupRepo) GetList(opts ...DBOption) ([]model.Group, error) { - var groups []model.Group - db := global.DB.Model(&model.Group{}) - for _, opt := range opts { - db = opt(db) - } - err := db.Find(&groups).Error - return groups, err -} - -func (u *GroupRepo) Create(group *model.Group) error { - return global.DB.Create(group).Error -} - -func (u *GroupRepo) Update(id uint, vars map[string]interface{}) error { - return global.DB.Model(&model.Group{}).Where("id = ?", id).Updates(vars).Error -} - -func (u *GroupRepo) WithByHostDefault() DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("is_default = ? AND type = ?", 1, "host") - } -} - -func (u *GroupRepo) Delete(opts ...DBOption) error { - db := global.DB - for _, opt := range opts { - db = opt(db) - } - return db.Delete(&model.Group{}).Error -} - -func (u *GroupRepo) CancelDefault(groupType string) error { - return global.DB.Model(&model.Group{}).Where("is_default = ? AND type = ?", 1, groupType).Updates(map[string]interface{}{"is_default": 0}).Error -} diff --git a/backend/app/repo/host.go b/backend/app/repo/host.go deleted file mode 100644 index 0ca114a1c..000000000 --- a/backend/app/repo/host.go +++ /dev/null @@ -1,159 +0,0 @@ -package repo - -import ( - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" - "gorm.io/gorm" -) - -type HostRepo struct{} - -type IHostRepo interface { - Get(opts ...DBOption) (model.Host, error) - GetList(opts ...DBOption) ([]model.Host, error) - Page(limit, offset int, opts ...DBOption) (int64, []model.Host, error) - WithByInfo(info string) DBOption - WithByPort(port uint) DBOption - WithByUser(user string) DBOption - WithByAddr(addr string) DBOption - Create(host *model.Host) error - Update(id uint, vars map[string]interface{}) error - Delete(opts ...DBOption) error - - GetFirewallRecord(opts ...DBOption) (model.Firewall, error) - ListFirewallRecord() ([]model.Firewall, error) - SaveFirewallRecord(firewall *model.Firewall) error - DeleteFirewallRecordByID(id uint) error - DeleteFirewallRecord(fType, port, protocol, address, strategy string) error -} - -func NewIHostRepo() IHostRepo { - return &HostRepo{} -} - -func (h *HostRepo) Get(opts ...DBOption) (model.Host, error) { - var host model.Host - db := global.DB - for _, opt := range opts { - db = opt(db) - } - err := db.First(&host).Error - return host, err -} - -func (h *HostRepo) GetList(opts ...DBOption) ([]model.Host, error) { - var hosts []model.Host - db := global.DB.Model(&model.Host{}) - for _, opt := range opts { - db = opt(db) - } - err := db.Find(&hosts).Error - return hosts, err -} - -func (h *HostRepo) Page(page, size int, opts ...DBOption) (int64, []model.Host, error) { - var users []model.Host - db := global.DB.Model(&model.Host{}) - for _, opt := range opts { - db = opt(db) - } - count := int64(0) - db = db.Count(&count) - err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error - return count, users, err -} - -func (h *HostRepo) WithByInfo(info string) DBOption { - return func(g *gorm.DB) *gorm.DB { - if len(info) == 0 { - return g - } - infoStr := "%" + info + "%" - return g.Where("name LIKE ? OR addr LIKE ?", infoStr, infoStr) - } -} - -func (h *HostRepo) WithByPort(port uint) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("port = ?", port) - } -} -func (h *HostRepo) WithByUser(user string) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("user = ?", user) - } -} -func (h *HostRepo) WithByAddr(addr string) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("addr = ?", addr) - } -} -func (h *HostRepo) WithByGroup(group string) DBOption { - return func(g *gorm.DB) *gorm.DB { - if len(group) == 0 { - return g - } - return g.Where("group_belong = ?", group) - } -} - -func (h *HostRepo) Create(host *model.Host) error { - return global.DB.Create(host).Error -} - -func (h *HostRepo) Update(id uint, vars map[string]interface{}) error { - return global.DB.Model(&model.Host{}).Where("id = ?", id).Updates(vars).Error -} - -func (h *HostRepo) Delete(opts ...DBOption) error { - db := global.DB - for _, opt := range opts { - db = opt(db) - } - return db.Delete(&model.Host{}).Error -} - -func (h *HostRepo) GetFirewallRecord(opts ...DBOption) (model.Firewall, error) { - var firewall model.Firewall - db := global.DB - for _, opt := range opts { - db = opt(db) - } - err := db.First(&firewall).Error - return firewall, err -} - -func (h *HostRepo) ListFirewallRecord() ([]model.Firewall, error) { - var datas []model.Firewall - if err := global.DB.Find(&datas).Error; err != nil { - return datas, nil - } - return datas, nil -} - -func (h *HostRepo) SaveFirewallRecord(firewall *model.Firewall) error { - if firewall.ID != 0 { - return global.DB.Save(firewall).Error - } - var data model.Firewall - if firewall.Type == "port" { - _ = global.DB.Where("type = ? AND port = ? AND protocol = ? AND address = ? AND strategy = ?", "port", firewall.Port, firewall.Protocol, firewall.Address, firewall.Strategy).First(&data) - if data.ID != 0 { - firewall.ID = data.ID - } - } else { - _ = global.DB.Where("type = ? AND address = ? AND strategy = ?", "address", firewall.Address, firewall.Strategy).First(&data) - if data.ID != 0 { - firewall.ID = data.ID - } - } - return global.DB.Save(firewall).Error -} - -func (h *HostRepo) DeleteFirewallRecordByID(id uint) error { - return global.DB.Where("id = ?", id).Delete(&model.Firewall{}).Error -} - -func (h *HostRepo) DeleteFirewallRecord(fType, port, protocol, address, strategy string) error { - return global.DB.Where("type = ? AND port = ? AND protocol = ? AND address = ? AND strategy = ?", fType, port, protocol, address, strategy).Delete(&model.Firewall{}).Error -} diff --git a/backend/app/repo/image_repo.go b/backend/app/repo/image_repo.go deleted file mode 100644 index 79e7e95c9..000000000 --- a/backend/app/repo/image_repo.go +++ /dev/null @@ -1,71 +0,0 @@ -package repo - -import ( - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" -) - -type ImageRepoRepo struct{} - -type IImageRepoRepo interface { - Get(opts ...DBOption) (model.ImageRepo, error) - Page(limit, offset int, opts ...DBOption) (int64, []model.ImageRepo, error) - List(opts ...DBOption) ([]model.ImageRepo, error) - Create(imageRepo *model.ImageRepo) error - Update(id uint, vars map[string]interface{}) error - Delete(opts ...DBOption) error -} - -func NewIImageRepoRepo() IImageRepoRepo { - return &ImageRepoRepo{} -} - -func (u *ImageRepoRepo) Get(opts ...DBOption) (model.ImageRepo, error) { - var imageRepo model.ImageRepo - db := global.DB - for _, opt := range opts { - db = opt(db) - } - err := db.First(&imageRepo).Error - return imageRepo, err -} - -func (u *ImageRepoRepo) Page(page, size int, opts ...DBOption) (int64, []model.ImageRepo, error) { - var ops []model.ImageRepo - db := global.DB.Model(&model.ImageRepo{}) - for _, opt := range opts { - db = opt(db) - } - count := int64(0) - db = db.Count(&count) - err := db.Limit(size).Offset(size * (page - 1)).Find(&ops).Error - return count, ops, err -} - -func (u *ImageRepoRepo) List(opts ...DBOption) ([]model.ImageRepo, error) { - var ops []model.ImageRepo - db := global.DB.Model(&model.ImageRepo{}) - for _, opt := range opts { - db = opt(db) - } - count := int64(0) - db = db.Count(&count) - err := db.Find(&ops).Error - return ops, err -} - -func (u *ImageRepoRepo) Create(imageRepo *model.ImageRepo) error { - return global.DB.Create(imageRepo).Error -} - -func (u *ImageRepoRepo) Update(id uint, vars map[string]interface{}) error { - return global.DB.Model(&model.ImageRepo{}).Where("id = ?", id).Updates(vars).Error -} - -func (u *ImageRepoRepo) Delete(opts ...DBOption) error { - db := global.DB - for _, opt := range opts { - db = opt(db) - } - return db.Delete(&model.ImageRepo{}).Error -} diff --git a/backend/app/repo/php_extensions.go b/backend/app/repo/php_extensions.go deleted file mode 100644 index 81da5f364..000000000 --- a/backend/app/repo/php_extensions.go +++ /dev/null @@ -1,59 +0,0 @@ -package repo - -import ( - "github.com/1Panel-dev/1Panel/backend/app/model" -) - -type PHPExtensionsRepo struct { -} - -type IPHPExtensionsRepo interface { - Page(page, size int, opts ...DBOption) (int64, []model.PHPExtensions, error) - Save(extension *model.PHPExtensions) error - Create(extension *model.PHPExtensions) error - GetFirst(opts ...DBOption) (model.PHPExtensions, error) - DeleteBy(opts ...DBOption) error - List() ([]model.PHPExtensions, error) -} - -func NewIPHPExtensionsRepo() IPHPExtensionsRepo { - return &PHPExtensionsRepo{} -} - -func (p *PHPExtensionsRepo) Page(page, size int, opts ...DBOption) (int64, []model.PHPExtensions, error) { - var ( - phpExtensions []model.PHPExtensions - ) - db := getDb(opts...).Model(&model.PHPExtensions{}) - count := int64(0) - db = db.Count(&count) - err := db.Limit(size).Offset(size * (page - 1)).Find(&phpExtensions).Error - return count, phpExtensions, err -} - -func (p *PHPExtensionsRepo) List() ([]model.PHPExtensions, error) { - var ( - phpExtensions []model.PHPExtensions - ) - err := getDb().Model(&model.PHPExtensions{}).Find(&phpExtensions).Error - return phpExtensions, err -} - -func (p *PHPExtensionsRepo) Save(extension *model.PHPExtensions) error { - return getDb().Save(&extension).Error -} - -func (p *PHPExtensionsRepo) Create(extension *model.PHPExtensions) error { - return getDb().Create(&extension).Error -} - -func (p *PHPExtensionsRepo) GetFirst(opts ...DBOption) (model.PHPExtensions, error) { - var extension model.PHPExtensions - db := getDb(opts...).Model(&model.PHPExtensions{}) - err := db.First(&extension).Error - return extension, err -} - -func (p *PHPExtensionsRepo) DeleteBy(opts ...DBOption) error { - return getDb(opts...).Delete(&model.PHPExtensions{}).Error -} diff --git a/backend/app/repo/runtime.go b/backend/app/repo/runtime.go deleted file mode 100644 index 489743c62..000000000 --- a/backend/app/repo/runtime.go +++ /dev/null @@ -1,107 +0,0 @@ -package repo - -import ( - "context" - - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" - "gorm.io/gorm" -) - -type RuntimeRepo struct { -} - -type IRuntimeRepo interface { - WithName(name string) DBOption - WithImage(image string) DBOption - WithNotId(id uint) DBOption - WithStatus(status string) DBOption - WithDetailId(id uint) DBOption - WithPort(port int) DBOption - Page(page, size int, opts ...DBOption) (int64, []model.Runtime, error) - Create(ctx context.Context, runtime *model.Runtime) error - Save(runtime *model.Runtime) error - DeleteBy(opts ...DBOption) error - GetFirst(opts ...DBOption) (*model.Runtime, error) - List(opts ...DBOption) ([]model.Runtime, error) -} - -func NewIRunTimeRepo() IRuntimeRepo { - return &RuntimeRepo{} -} - -func (r *RuntimeRepo) WithName(name string) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("name = ?", name) - } -} - -func (r *RuntimeRepo) WithStatus(status string) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("status = ?", status) - } -} - -func (r *RuntimeRepo) WithImage(image string) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("image = ?", image) - } -} - -func (r *RuntimeRepo) WithDetailId(id uint) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("app_detail_id = ?", id) - } -} - -func (r *RuntimeRepo) WithNotId(id uint) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("id != ?", id) - } -} - -func (r *RuntimeRepo) WithPort(port int) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("port = ?", port) - } -} - -func (r *RuntimeRepo) Page(page, size int, opts ...DBOption) (int64, []model.Runtime, error) { - var runtimes []model.Runtime - db := getDb(opts...).Model(&model.Runtime{}) - count := int64(0) - db = db.Count(&count) - err := db.Limit(size).Offset(size * (page - 1)).Find(&runtimes).Error - return count, runtimes, err -} - -func (r *RuntimeRepo) List(opts ...DBOption) ([]model.Runtime, error) { - var runtimes []model.Runtime - db := global.DB.Model(&model.Runtime{}) - for _, opt := range opts { - db = opt(db) - } - err := db.Find(&runtimes).Error - return runtimes, err -} - -func (r *RuntimeRepo) Create(ctx context.Context, runtime *model.Runtime) error { - db := getTx(ctx).Model(&model.Runtime{}) - return db.Create(&runtime).Error -} - -func (r *RuntimeRepo) Save(runtime *model.Runtime) error { - return getDb().Save(&runtime).Error -} - -func (r *RuntimeRepo) DeleteBy(opts ...DBOption) error { - return getDb(opts...).Delete(&model.Runtime{}).Error -} - -func (r *RuntimeRepo) GetFirst(opts ...DBOption) (*model.Runtime, error) { - var runtime model.Runtime - if err := getDb(opts...).First(&runtime).Error; err != nil { - return nil, err - } - return &runtime, nil -} diff --git a/backend/app/repo/setting.go b/backend/app/repo/setting.go deleted file mode 100644 index c5453189f..000000000 --- a/backend/app/repo/setting.go +++ /dev/null @@ -1,87 +0,0 @@ -package repo - -import ( - "time" - - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" - "gorm.io/gorm" -) - -type SettingRepo struct{} - -type ISettingRepo interface { - GetList(opts ...DBOption) ([]model.Setting, error) - Get(opts ...DBOption) (model.Setting, error) - Create(key, value string) error - Update(key, value string) error - WithByKey(key string) DBOption - - CreateMonitorBase(model model.MonitorBase) error - BatchCreateMonitorIO(ioList []model.MonitorIO) error - BatchCreateMonitorNet(ioList []model.MonitorNetwork) error - DelMonitorBase(timeForDelete time.Time) error - DelMonitorIO(timeForDelete time.Time) error - DelMonitorNet(timeForDelete time.Time) error -} - -func NewISettingRepo() ISettingRepo { - return &SettingRepo{} -} - -func (u *SettingRepo) GetList(opts ...DBOption) ([]model.Setting, error) { - var settings []model.Setting - db := global.DB.Model(&model.Setting{}) - for _, opt := range opts { - db = opt(db) - } - err := db.Find(&settings).Error - return settings, err -} - -func (u *SettingRepo) Create(key, value string) error { - setting := &model.Setting{ - Key: key, - Value: value, - } - return global.DB.Create(setting).Error -} - -func (u *SettingRepo) Get(opts ...DBOption) (model.Setting, error) { - var settings model.Setting - db := global.DB.Model(&model.Setting{}) - for _, opt := range opts { - db = opt(db) - } - err := db.First(&settings).Error - return settings, err -} - -func (c *SettingRepo) WithByKey(key string) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("key = ?", key) - } -} - -func (u *SettingRepo) Update(key, value string) error { - return global.DB.Model(&model.Setting{}).Where("key = ?", key).Updates(map[string]interface{}{"value": value}).Error -} - -func (u *SettingRepo) CreateMonitorBase(model model.MonitorBase) error { - return global.MonitorDB.Create(&model).Error -} -func (u *SettingRepo) BatchCreateMonitorIO(ioList []model.MonitorIO) error { - return global.MonitorDB.CreateInBatches(ioList, len(ioList)).Error -} -func (u *SettingRepo) BatchCreateMonitorNet(ioList []model.MonitorNetwork) error { - return global.MonitorDB.CreateInBatches(ioList, len(ioList)).Error -} -func (u *SettingRepo) DelMonitorBase(timeForDelete time.Time) error { - return global.MonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorBase{}).Error -} -func (u *SettingRepo) DelMonitorIO(timeForDelete time.Time) error { - return global.MonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorIO{}).Error -} -func (u *SettingRepo) DelMonitorNet(timeForDelete time.Time) error { - return global.MonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorNetwork{}).Error -} diff --git a/backend/app/repo/snapshot.go b/backend/app/repo/snapshot.go deleted file mode 100644 index 0b6eec6f8..000000000 --- a/backend/app/repo/snapshot.go +++ /dev/null @@ -1,105 +0,0 @@ -package repo - -import ( - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" -) - -type ISnapshotRepo interface { - Get(opts ...DBOption) (model.Snapshot, error) - GetList(opts ...DBOption) ([]model.Snapshot, error) - Create(snap *model.Snapshot) error - Update(id uint, vars map[string]interface{}) error - Page(limit, offset int, opts ...DBOption) (int64, []model.Snapshot, error) - Delete(opts ...DBOption) error - - GetStatus(snapID uint) (model.SnapshotStatus, error) - GetStatusList(opts ...DBOption) ([]model.SnapshotStatus, error) - CreateStatus(snap *model.SnapshotStatus) error - DeleteStatus(snapID uint) error - UpdateStatus(id uint, vars map[string]interface{}) error -} - -func NewISnapshotRepo() ISnapshotRepo { - return &SnapshotRepo{} -} - -type SnapshotRepo struct{} - -func (u *SnapshotRepo) Get(opts ...DBOption) (model.Snapshot, error) { - var Snapshot model.Snapshot - db := global.DB - for _, opt := range opts { - db = opt(db) - } - err := db.First(&Snapshot).Error - return Snapshot, err -} - -func (u *SnapshotRepo) GetList(opts ...DBOption) ([]model.Snapshot, error) { - var snaps []model.Snapshot - db := global.DB.Model(&model.Snapshot{}) - for _, opt := range opts { - db = opt(db) - } - err := db.Find(&snaps).Error - return snaps, err -} - -func (u *SnapshotRepo) Page(page, size int, opts ...DBOption) (int64, []model.Snapshot, error) { - var users []model.Snapshot - db := global.DB.Model(&model.Snapshot{}) - for _, opt := range opts { - db = opt(db) - } - count := int64(0) - db = db.Count(&count) - err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error - return count, users, err -} - -func (u *SnapshotRepo) Create(Snapshot *model.Snapshot) error { - return global.DB.Create(Snapshot).Error -} - -func (u *SnapshotRepo) Update(id uint, vars map[string]interface{}) error { - return global.DB.Model(&model.Snapshot{}).Where("id = ?", id).Updates(vars).Error -} - -func (u *SnapshotRepo) Delete(opts ...DBOption) error { - db := global.DB - for _, opt := range opts { - db = opt(db) - } - return db.Delete(&model.Snapshot{}).Error -} - -func (u *SnapshotRepo) GetStatus(snapID uint) (model.SnapshotStatus, error) { - var data model.SnapshotStatus - if err := global.DB.Where("snap_id = ?", snapID).First(&data).Error; err != nil { - return data, err - } - return data, nil -} - -func (u *SnapshotRepo) GetStatusList(opts ...DBOption) ([]model.SnapshotStatus, error) { - var status []model.SnapshotStatus - db := global.DB.Model(&model.SnapshotStatus{}) - for _, opt := range opts { - db = opt(db) - } - err := db.Find(&status).Error - return status, err -} - -func (u *SnapshotRepo) CreateStatus(snap *model.SnapshotStatus) error { - return global.DB.Create(snap).Error -} - -func (u *SnapshotRepo) DeleteStatus(snapID uint) error { - return global.DB.Where("snap_id = ?", snapID).Delete(&model.SnapshotStatus{}).Error -} - -func (u *SnapshotRepo) UpdateStatus(id uint, vars map[string]interface{}) error { - return global.DB.Model(&model.SnapshotStatus{}).Where("id = ?", id).Updates(vars).Error -} diff --git a/backend/app/repo/tag.go b/backend/app/repo/tag.go deleted file mode 100644 index 122792c14..000000000 --- a/backend/app/repo/tag.go +++ /dev/null @@ -1,62 +0,0 @@ -package repo - -import ( - "context" - "github.com/1Panel-dev/1Panel/backend/app/model" -) - -type TagRepo struct { -} - -type ITagRepo interface { - BatchCreate(ctx context.Context, tags []*model.Tag) error - DeleteAll(ctx context.Context) error - All() ([]model.Tag, error) - GetByIds(ids []uint) ([]model.Tag, error) - GetByKeys(keys []string) ([]model.Tag, error) - GetByAppId(appId uint) ([]model.Tag, error) -} - -func NewITagRepo() ITagRepo { - return &TagRepo{} -} - -func (t TagRepo) BatchCreate(ctx context.Context, tags []*model.Tag) error { - return getTx(ctx).Create(&tags).Error -} - -func (t TagRepo) DeleteAll(ctx context.Context) error { - return getTx(ctx).Where("1 = 1 ").Delete(&model.Tag{}).Error -} - -func (t TagRepo) All() ([]model.Tag, error) { - var tags []model.Tag - if err := getDb().Where("1 = 1 ").Order("sort asc").Find(&tags).Error; err != nil { - return nil, err - } - return tags, nil -} - -func (t TagRepo) GetByIds(ids []uint) ([]model.Tag, error) { - var tags []model.Tag - if err := getDb().Where("id in (?)", ids).Find(&tags).Error; err != nil { - return nil, err - } - return tags, nil -} - -func (t TagRepo) GetByKeys(keys []string) ([]model.Tag, error) { - var tags []model.Tag - if err := getDb().Where("key in (?)", keys).Find(&tags).Error; err != nil { - return nil, err - } - return tags, nil -} - -func (t TagRepo) GetByAppId(appId uint) ([]model.Tag, error) { - var tags []model.Tag - if err := getDb().Where("id in (select tag_id from app_tags where app_id = ?)", appId).Find(&tags).Error; err != nil { - return nil, err - } - return tags, nil -} diff --git a/backend/app/repo/website.go b/backend/app/repo/website.go deleted file mode 100644 index 50f453ac1..000000000 --- a/backend/app/repo/website.go +++ /dev/null @@ -1,145 +0,0 @@ -package repo - -import ( - "context" - - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -type IWebsiteRepo interface { - WithAppInstallId(appInstallId uint) DBOption - WithDomain(domain string) DBOption - WithAlias(alias string) DBOption - WithWebsiteSSLID(sslId uint) DBOption - WithGroupID(groupId uint) DBOption - WithDefaultServer() DBOption - WithDomainLike(domain string) DBOption - WithRuntimeID(runtimeID uint) DBOption - WithIDs(ids []uint) DBOption - Page(page, size int, opts ...DBOption) (int64, []model.Website, error) - List(opts ...DBOption) ([]model.Website, error) - GetFirst(opts ...DBOption) (model.Website, error) - GetBy(opts ...DBOption) ([]model.Website, error) - Save(ctx context.Context, app *model.Website) error - SaveWithoutCtx(app *model.Website) error - DeleteBy(ctx context.Context, opts ...DBOption) error - Create(ctx context.Context, app *model.Website) error - DeleteAll(ctx context.Context) error -} - -func NewIWebsiteRepo() IWebsiteRepo { - return &WebsiteRepo{} -} - -type WebsiteRepo struct { -} - -func (w *WebsiteRepo) WithAppInstallId(appInstallID uint) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("app_install_id = ?", appInstallID) - } -} - -func (w *WebsiteRepo) WithIDs(ids []uint) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("id in (?)", ids) - } -} - -func (w *WebsiteRepo) WithRuntimeID(runtimeID uint) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("runtime_id = ?", runtimeID) - } -} - -func (w *WebsiteRepo) WithDomain(domain string) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("primary_domain = ?", domain) - } -} - -func (w *WebsiteRepo) WithDomainLike(domain string) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("primary_domain like ?", "%"+domain+"%") - } -} - -func (w *WebsiteRepo) WithAlias(alias string) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("alias = ?", alias) - } -} - -func (w *WebsiteRepo) WithWebsiteSSLID(sslId uint) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("website_ssl_id = ?", sslId) - } -} - -func (w *WebsiteRepo) WithGroupID(groupId uint) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("website_group_id = ?", groupId) - } -} - -func (w *WebsiteRepo) WithDefaultServer() DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("default_server = 1") - } -} - -func (w *WebsiteRepo) Page(page, size int, opts ...DBOption) (int64, []model.Website, error) { - var websites []model.Website - db := getDb(opts...).Model(&model.Website{}) - count := int64(0) - db = db.Count(&count) - err := db.Debug().Limit(size).Offset(size * (page - 1)).Preload("WebsiteSSL").Find(&websites).Error - return count, websites, err -} - -func (w *WebsiteRepo) List(opts ...DBOption) ([]model.Website, error) { - var websites []model.Website - err := getDb(opts...).Model(&model.Website{}).Preload("Domains").Preload("WebsiteSSL").Find(&websites).Error - return websites, err -} - -func (w *WebsiteRepo) GetFirst(opts ...DBOption) (model.Website, error) { - var website model.Website - db := getDb(opts...).Model(&model.Website{}) - if err := db.Preload("Domains").First(&website).Error; err != nil { - return website, err - } - return website, nil -} - -func (w *WebsiteRepo) GetBy(opts ...DBOption) ([]model.Website, error) { - var websites []model.Website - db := getDb(opts...).Model(&model.Website{}) - if err := db.Find(&websites).Error; err != nil { - return websites, err - } - return websites, nil -} - -func (w *WebsiteRepo) Create(ctx context.Context, app *model.Website) error { - return getTx(ctx).Omit(clause.Associations).Create(app).Error -} - -func (w *WebsiteRepo) Save(ctx context.Context, app *model.Website) error { - return getTx(ctx).Omit(clause.Associations).Save(app).Error -} - -func (w *WebsiteRepo) SaveWithoutCtx(website *model.Website) error { - return global.DB.Save(website).Error -} - -func (w *WebsiteRepo) DeleteBy(ctx context.Context, opts ...DBOption) error { - return getTx(ctx, opts...).Delete(&model.Website{}).Error -} - -func (w *WebsiteRepo) DeleteAll(ctx context.Context) error { - return getTx(ctx).Where("1 = 1 ").Delete(&model.Website{}).Error -} diff --git a/backend/app/repo/website_acme_account.go b/backend/app/repo/website_acme_account.go deleted file mode 100644 index c2ebee637..000000000 --- a/backend/app/repo/website_acme_account.go +++ /dev/null @@ -1,64 +0,0 @@ -package repo - -import ( - "github.com/1Panel-dev/1Panel/backend/app/model" - "gorm.io/gorm" -) - -type IAcmeAccountRepo interface { - Page(page, size int, opts ...DBOption) (int64, []model.WebsiteAcmeAccount, error) - GetFirst(opts ...DBOption) (*model.WebsiteAcmeAccount, error) - Create(account model.WebsiteAcmeAccount) error - Save(account model.WebsiteAcmeAccount) error - DeleteBy(opts ...DBOption) error - WithEmail(email string) DBOption - WithType(acType string) DBOption -} - -func NewIAcmeAccountRepo() IAcmeAccountRepo { - return &WebsiteAcmeAccountRepo{} -} - -type WebsiteAcmeAccountRepo struct { -} - -func (w *WebsiteAcmeAccountRepo) WithEmail(email string) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("email = ?", email) - } -} -func (w *WebsiteAcmeAccountRepo) WithType(acType string) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("type = ?", acType) - } -} - -func (w *WebsiteAcmeAccountRepo) Page(page, size int, opts ...DBOption) (int64, []model.WebsiteAcmeAccount, error) { - var accounts []model.WebsiteAcmeAccount - db := getDb(opts...).Model(&model.WebsiteAcmeAccount{}) - count := int64(0) - db = db.Count(&count) - err := db.Limit(size).Offset(size * (page - 1)).Find(&accounts).Error - return count, accounts, err -} - -func (w *WebsiteAcmeAccountRepo) GetFirst(opts ...DBOption) (*model.WebsiteAcmeAccount, error) { - var account model.WebsiteAcmeAccount - db := getDb(opts...).Model(&model.WebsiteAcmeAccount{}) - if err := db.First(&account).Error; err != nil { - return nil, err - } - return &account, nil -} - -func (w *WebsiteAcmeAccountRepo) Create(account model.WebsiteAcmeAccount) error { - return getDb().Create(&account).Error -} - -func (w *WebsiteAcmeAccountRepo) Save(account model.WebsiteAcmeAccount) error { - return getDb().Save(&account).Error -} - -func (w *WebsiteAcmeAccountRepo) DeleteBy(opts ...DBOption) error { - return getDb(opts...).Debug().Delete(&model.WebsiteAcmeAccount{}).Error -} diff --git a/backend/app/repo/website_ca.go b/backend/app/repo/website_ca.go deleted file mode 100644 index d9c9ae5f1..000000000 --- a/backend/app/repo/website_ca.go +++ /dev/null @@ -1,54 +0,0 @@ -package repo - -import ( - "context" - "github.com/1Panel-dev/1Panel/backend/app/model" -) - -type WebsiteCARepo struct { -} - -func NewIWebsiteCARepo() IWebsiteCARepo { - return &WebsiteCARepo{} -} - -type IWebsiteCARepo interface { - Page(page, size int, opts ...DBOption) (int64, []model.WebsiteCA, error) - GetFirst(opts ...DBOption) (model.WebsiteCA, error) - List(opts ...DBOption) ([]model.WebsiteCA, error) - Create(ctx context.Context, ca *model.WebsiteCA) error - DeleteBy(opts ...DBOption) error -} - -func (w WebsiteCARepo) Page(page, size int, opts ...DBOption) (int64, []model.WebsiteCA, error) { - var caList []model.WebsiteCA - db := getDb(opts...).Model(&model.WebsiteCA{}) - count := int64(0) - db = db.Count(&count) - err := db.Limit(size).Offset(size * (page - 1)).Find(&caList).Error - return count, caList, err -} - -func (w WebsiteCARepo) GetFirst(opts ...DBOption) (model.WebsiteCA, error) { - var ca model.WebsiteCA - db := getDb(opts...).Model(&model.WebsiteCA{}) - if err := db.First(&ca).Error; err != nil { - return ca, err - } - return ca, nil -} - -func (w WebsiteCARepo) List(opts ...DBOption) ([]model.WebsiteCA, error) { - var caList []model.WebsiteCA - db := getDb(opts...).Model(&model.WebsiteCA{}) - err := db.Find(&caList).Error - return caList, err -} - -func (w WebsiteCARepo) Create(ctx context.Context, ca *model.WebsiteCA) error { - return getTx(ctx).Create(ca).Error -} - -func (w WebsiteCARepo) DeleteBy(opts ...DBOption) error { - return getDb(opts...).Delete(&model.WebsiteCA{}).Error -} diff --git a/backend/app/repo/website_dns_account.go b/backend/app/repo/website_dns_account.go deleted file mode 100644 index 56811b975..000000000 --- a/backend/app/repo/website_dns_account.go +++ /dev/null @@ -1,60 +0,0 @@ -package repo - -import ( - "github.com/1Panel-dev/1Panel/backend/app/model" -) - -type WebsiteDnsAccountRepo struct { -} - -type IWebsiteDnsAccountRepo interface { - Page(page, size int, opts ...DBOption) (int64, []model.WebsiteDnsAccount, error) - GetFirst(opts ...DBOption) (*model.WebsiteDnsAccount, error) - List(opts ...DBOption) ([]model.WebsiteDnsAccount, error) - Create(account model.WebsiteDnsAccount) error - Save(account model.WebsiteDnsAccount) error - DeleteBy(opts ...DBOption) error -} - -func NewIWebsiteDnsAccountRepo() IWebsiteDnsAccountRepo { - return &WebsiteDnsAccountRepo{} -} - -func (w WebsiteDnsAccountRepo) Page(page, size int, opts ...DBOption) (int64, []model.WebsiteDnsAccount, error) { - var accounts []model.WebsiteDnsAccount - db := getDb(opts...).Model(&model.WebsiteDnsAccount{}) - count := int64(0) - db = db.Count(&count) - err := db.Limit(size).Offset(size * (page - 1)).Find(&accounts).Error - return count, accounts, err -} - -func (w WebsiteDnsAccountRepo) GetFirst(opts ...DBOption) (*model.WebsiteDnsAccount, error) { - var account model.WebsiteDnsAccount - db := getDb(opts...).Model(&model.WebsiteDnsAccount{}) - if err := db.First(&account).Error; err != nil { - return nil, err - } - return &account, nil -} - -func (w WebsiteDnsAccountRepo) List(opts ...DBOption) ([]model.WebsiteDnsAccount, error) { - var accounts []model.WebsiteDnsAccount - db := getDb(opts...).Model(&model.WebsiteDnsAccount{}) - if err := db.Find(&accounts).Error; err != nil { - return nil, err - } - return accounts, nil -} - -func (w WebsiteDnsAccountRepo) Create(account model.WebsiteDnsAccount) error { - return getDb().Create(&account).Error -} - -func (w WebsiteDnsAccountRepo) Save(account model.WebsiteDnsAccount) error { - return getDb().Save(&account).Error -} - -func (w WebsiteDnsAccountRepo) DeleteBy(opts ...DBOption) error { - return getDb(opts...).Delete(&model.WebsiteDnsAccount{}).Error -} diff --git a/backend/app/repo/website_domain.go b/backend/app/repo/website_domain.go deleted file mode 100644 index fa5c45560..000000000 --- a/backend/app/repo/website_domain.go +++ /dev/null @@ -1,98 +0,0 @@ -package repo - -import ( - "context" - "github.com/1Panel-dev/1Panel/backend/app/model" - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -type WebsiteDomainRepo struct { -} - -type IWebsiteDomainRepo interface { - WithWebsiteId(websiteId uint) DBOption - WithPort(port int) DBOption - WithDomain(domain string) DBOption - WithDomainLike(domain string) DBOption - Page(page, size int, opts ...DBOption) (int64, []model.WebsiteDomain, error) - GetFirst(opts ...DBOption) (model.WebsiteDomain, error) - GetBy(opts ...DBOption) ([]model.WebsiteDomain, error) - BatchCreate(ctx context.Context, domains []model.WebsiteDomain) error - Create(ctx context.Context, app *model.WebsiteDomain) error - Save(ctx context.Context, app *model.WebsiteDomain) error - DeleteBy(ctx context.Context, opts ...DBOption) error - DeleteAll(ctx context.Context) error -} - -func NewIWebsiteDomainRepo() IWebsiteDomainRepo { - return &WebsiteDomainRepo{} -} - -func (w WebsiteDomainRepo) WithWebsiteId(websiteId uint) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("website_id = ?", websiteId) - } -} - -func (w WebsiteDomainRepo) WithPort(port int) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("port = ?", port) - } -} -func (w WebsiteDomainRepo) WithDomain(domain string) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("domain = ?", domain) - } -} -func (w WebsiteDomainRepo) WithDomainLike(domain string) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("domain like ?", "%"+domain+"%") - } -} -func (w WebsiteDomainRepo) Page(page, size int, opts ...DBOption) (int64, []model.WebsiteDomain, error) { - var domains []model.WebsiteDomain - db := getDb(opts...).Model(&model.WebsiteDomain{}) - count := int64(0) - db = db.Count(&count) - err := db.Limit(size).Offset(size * (page - 1)).Find(&domains).Error - return count, domains, err -} - -func (w WebsiteDomainRepo) GetFirst(opts ...DBOption) (model.WebsiteDomain, error) { - var domain model.WebsiteDomain - db := getDb(opts...).Model(&model.WebsiteDomain{}) - if err := db.First(&domain).Error; err != nil { - return domain, err - } - return domain, nil -} - -func (w WebsiteDomainRepo) GetBy(opts ...DBOption) ([]model.WebsiteDomain, error) { - var domains []model.WebsiteDomain - db := getDb(opts...).Model(&model.WebsiteDomain{}) - if err := db.Find(&domains).Error; err != nil { - return domains, err - } - return domains, nil -} - -func (w WebsiteDomainRepo) BatchCreate(ctx context.Context, domains []model.WebsiteDomain) error { - return getTx(ctx).Model(&model.WebsiteDomain{}).Create(&domains).Error -} - -func (w WebsiteDomainRepo) Create(ctx context.Context, app *model.WebsiteDomain) error { - return getTx(ctx).Omit(clause.Associations).Create(app).Error -} - -func (w WebsiteDomainRepo) Save(ctx context.Context, app *model.WebsiteDomain) error { - return getTx(ctx).Omit(clause.Associations).Save(app).Error -} - -func (w WebsiteDomainRepo) DeleteBy(ctx context.Context, opts ...DBOption) error { - return getTx(ctx, opts...).Delete(&model.WebsiteDomain{}).Error -} - -func (w WebsiteDomainRepo) DeleteAll(ctx context.Context) error { - return getTx(ctx).Where("1 = 1 ").Delete(&model.WebsiteDomain{}).Error -} diff --git a/backend/app/repo/website_ssl.go b/backend/app/repo/website_ssl.go deleted file mode 100644 index bac38543b..000000000 --- a/backend/app/repo/website_ssl.go +++ /dev/null @@ -1,99 +0,0 @@ -package repo - -import ( - "context" - "github.com/1Panel-dev/1Panel/backend/app/model" - "gorm.io/gorm" -) - -func NewISSLRepo() ISSLRepo { - return &WebsiteSSLRepo{} -} - -type ISSLRepo interface { - WithByAlias(alias string) DBOption - WithByAcmeAccountId(acmeAccountId uint) DBOption - WithByDnsAccountId(dnsAccountId uint) DBOption - WithByCAID(caID uint) DBOption - Page(page, size int, opts ...DBOption) (int64, []model.WebsiteSSL, error) - GetFirst(opts ...DBOption) (*model.WebsiteSSL, error) - List(opts ...DBOption) ([]model.WebsiteSSL, error) - Create(ctx context.Context, ssl *model.WebsiteSSL) error - Save(ssl *model.WebsiteSSL) error - DeleteBy(opts ...DBOption) error - SaveByMap(ssl *model.WebsiteSSL, params map[string]interface{}) error -} - -type WebsiteSSLRepo struct { -} - -func (w WebsiteSSLRepo) WithByAlias(alias string) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("alias = ?", alias) - } -} - -func (w WebsiteSSLRepo) WithByAcmeAccountId(acmeAccountId uint) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("acme_account_id = ?", acmeAccountId) - } -} - -func (w WebsiteSSLRepo) WithByDnsAccountId(dnsAccountId uint) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("dns_account_id = ?", dnsAccountId) - } -} - -func (w WebsiteSSLRepo) WithByCAID(caID uint) DBOption { - return func(db *gorm.DB) *gorm.DB { - return db.Where("ca_id = ?", caID) - } -} - -func (w WebsiteSSLRepo) Page(page, size int, opts ...DBOption) (int64, []model.WebsiteSSL, error) { - var sslList []model.WebsiteSSL - db := getDb(opts...).Model(&model.WebsiteSSL{}) - count := int64(0) - db = db.Count(&count) - err := db.Limit(size).Offset(size * (page - 1)).Preload("AcmeAccount").Preload("DnsAccount").Preload("Websites").Find(&sslList).Error - return count, sslList, err -} - -func (w WebsiteSSLRepo) GetFirst(opts ...DBOption) (*model.WebsiteSSL, error) { - var website *model.WebsiteSSL - db := getDb(opts...).Model(&model.WebsiteSSL{}) - if err := db.Preload("AcmeAccount").Preload("DnsAccount").First(&website).Error; err != nil { - return website, err - } - return website, nil -} - -func (w WebsiteSSLRepo) List(opts ...DBOption) ([]model.WebsiteSSL, error) { - var websites []model.WebsiteSSL - db := getDb(opts...).Model(&model.WebsiteSSL{}) - if err := db.Preload("AcmeAccount").Preload("DnsAccount").Find(&websites).Error; err != nil { - return websites, err - } - return websites, nil -} - -func (w WebsiteSSLRepo) Create(ctx context.Context, ssl *model.WebsiteSSL) error { - return getTx(ctx).Create(ssl).Error -} - -func (w WebsiteSSLRepo) Save(ssl *model.WebsiteSSL) error { - return getDb().Model(&model.WebsiteSSL{BaseModel: model.BaseModel{ - ID: ssl.ID, - }}).Save(&ssl).Error -} - -func (w WebsiteSSLRepo) SaveByMap(ssl *model.WebsiteSSL, params map[string]interface{}) error { - return getDb().Model(&model.WebsiteSSL{BaseModel: model.BaseModel{ - ID: ssl.ID, - }}).Updates(params).Error -} - -func (w WebsiteSSLRepo) DeleteBy(opts ...DBOption) error { - return getDb(opts...).Delete(&model.WebsiteSSL{}).Error -} diff --git a/backend/app/service/app.go b/backend/app/service/app.go deleted file mode 100644 index 2f557060d..000000000 --- a/backend/app/service/app.go +++ /dev/null @@ -1,1028 +0,0 @@ -package service - -import ( - "context" - "encoding/base64" - "encoding/json" - "fmt" - "net/http" - "os" - "path/filepath" - "reflect" - "strconv" - "strings" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/app/dto/response" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/app/repo" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/i18n" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/1Panel-dev/1Panel/backend/utils/docker" - "github.com/1Panel-dev/1Panel/backend/utils/files" - http2 "github.com/1Panel-dev/1Panel/backend/utils/http" - httpUtil "github.com/1Panel-dev/1Panel/backend/utils/http" - "github.com/1Panel-dev/1Panel/backend/utils/xpack" - "gopkg.in/yaml.v3" -) - -type AppService struct { -} - -type IAppService interface { - PageApp(req request.AppSearch) (interface{}, error) - GetAppTags() ([]response.TagDTO, error) - GetApp(key string) (*response.AppDTO, error) - GetAppDetail(appId uint, version, appType string) (response.AppDetailDTO, error) - Install(ctx context.Context, req request.AppInstallCreate) (*model.AppInstall, error) - SyncAppListFromRemote() error - GetAppUpdate() (*response.AppUpdateRes, error) - GetAppDetailByID(id uint) (*response.AppDetailDTO, error) - SyncAppListFromLocal() - GetIgnoredApp() ([]response.IgnoredApp, error) -} - -func NewIAppService() IAppService { - return &AppService{} -} - -func (a AppService) PageApp(req request.AppSearch) (interface{}, error) { - var opts []repo.DBOption - opts = append(opts, appRepo.OrderByRecommend()) - if req.Name != "" { - opts = append(opts, appRepo.WithLikeName(req.Name)) - } - if req.Type != "" { - opts = append(opts, appRepo.WithType(req.Type)) - } - if req.Recommend { - opts = append(opts, appRepo.GetRecommend()) - } - if req.Resource != "" && req.Resource != "all" { - opts = append(opts, appRepo.WithResource(req.Resource)) - } - if len(req.Tags) != 0 { - tags, err := tagRepo.GetByKeys(req.Tags) - if err != nil { - return nil, err - } - var tagIds []uint - for _, t := range tags { - tagIds = append(tagIds, t.ID) - } - appTags, err := appTagRepo.GetByTagIds(tagIds) - if err != nil { - return nil, err - } - var appIds []uint - for _, t := range appTags { - appIds = append(appIds, t.AppId) - } - opts = append(opts, commonRepo.WithIdsIn(appIds)) - } - var res response.AppRes - total, apps, err := appRepo.Page(req.Page, req.PageSize, opts...) - if err != nil { - return nil, err - } - var appDTOs []*response.AppDto - for _, ap := range apps { - ap.ReadMe = "" - ap.Website = "" - ap.Document = "" - ap.Github = "" - appDTO := &response.AppDto{ - ID: ap.ID, - Name: ap.Name, - Key: ap.Key, - Type: ap.Type, - Icon: ap.Icon, - ShortDescZh: ap.ShortDescZh, - ShortDescEn: ap.ShortDescEn, - Resource: ap.Resource, - Limit: ap.Limit, - } - appDTOs = append(appDTOs, appDTO) - appTags, err := appTagRepo.GetByAppId(ap.ID) - if err != nil { - continue - } - var tagIds []uint - for _, at := range appTags { - tagIds = append(tagIds, at.TagId) - } - tags, err := tagRepo.GetByIds(tagIds) - if err != nil { - continue - } - appDTO.Tags = tags - installs, _ := appInstallRepo.ListBy(appInstallRepo.WithAppId(ap.ID)) - appDTO.Installed = len(installs) > 0 - } - res.Items = appDTOs - res.Total = total - - return res, nil -} - -func (a AppService) GetAppTags() ([]response.TagDTO, error) { - tags, err := tagRepo.All() - if err != nil { - return nil, err - } - var res []response.TagDTO - for _, tag := range tags { - res = append(res, response.TagDTO{ - Tag: tag, - }) - } - return res, nil -} - -func (a AppService) GetApp(key string) (*response.AppDTO, error) { - var appDTO response.AppDTO - app, err := appRepo.GetFirst(appRepo.WithKey(key)) - if err != nil { - return nil, err - } - appDTO.App = app - details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(app.ID)) - if err != nil { - return nil, err - } - var versionsRaw []string - for _, detail := range details { - versionsRaw = append(versionsRaw, detail.Version) - } - appDTO.Versions = common.GetSortedVersions(versionsRaw) - - return &appDTO, nil -} - -func (a AppService) GetAppDetail(appID uint, version, appType string) (response.AppDetailDTO, error) { - var ( - appDetailDTO response.AppDetailDTO - opts []repo.DBOption - ) - opts = append(opts, appDetailRepo.WithAppId(appID), appDetailRepo.WithVersion(version)) - detail, err := appDetailRepo.GetFirst(opts...) - if err != nil { - return appDetailDTO, err - } - appDetailDTO.AppDetail = detail - appDetailDTO.Enable = true - - if appType == "runtime" { - app, err := appRepo.GetFirst(commonRepo.WithByID(appID)) - if err != nil { - return appDetailDTO, err - } - fileOp := files.NewFileOp() - - versionPath := filepath.Join(app.GetAppResourcePath(), detail.Version) - if !fileOp.Stat(versionPath) || detail.Update { - if err = downloadApp(app, detail, nil); err != nil { - return appDetailDTO, err - } - } - switch app.Type { - case constant.RuntimePHP: - buildPath := filepath.Join(versionPath, "build") - paramsPath := filepath.Join(buildPath, "config.json") - if !fileOp.Stat(paramsPath) { - return appDetailDTO, buserr.New(constant.ErrFileNotExist) - } - param, err := fileOp.GetContent(paramsPath) - if err != nil { - return appDetailDTO, err - } - paramMap := make(map[string]interface{}) - if err := json.Unmarshal(param, ¶mMap); err != nil { - return appDetailDTO, err - } - appDetailDTO.Params = paramMap - composePath := filepath.Join(buildPath, "docker-compose.yml") - if !fileOp.Stat(composePath) { - return appDetailDTO, buserr.New(constant.ErrFileNotExist) - } - compose, err := fileOp.GetContent(composePath) - if err != nil { - return appDetailDTO, err - } - composeMap := make(map[string]interface{}) - if err := yaml.Unmarshal(compose, &composeMap); err != nil { - return appDetailDTO, err - } - if service, ok := composeMap["services"]; ok { - servicesMap := service.(map[string]interface{}) - for k := range servicesMap { - appDetailDTO.Image = k - } - } - } - } else { - paramMap := make(map[string]interface{}) - if err := json.Unmarshal([]byte(detail.Params), ¶mMap); err != nil { - return appDetailDTO, err - } - appDetailDTO.Params = paramMap - } - - if appDetailDTO.DockerCompose == "" { - filename := filepath.Base(appDetailDTO.DownloadUrl) - dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(appDetailDTO.DownloadUrl, filename), "docker-compose.yml") - statusCode, composeRes, err := httpUtil.HandleGet(dockerComposeUrl, http.MethodGet, constant.TimeOut20s) - if err != nil { - return appDetailDTO, buserr.WithDetail("ErrGetCompose", err.Error(), err) - } - if statusCode > 200 { - return appDetailDTO, buserr.WithDetail("ErrGetCompose", string(composeRes), err) - } - detail.DockerCompose = string(composeRes) - _ = appDetailRepo.Update(context.Background(), detail) - appDetailDTO.DockerCompose = string(composeRes) - } - - appDetailDTO.HostMode = isHostModel(appDetailDTO.DockerCompose) - - app, err := appRepo.GetFirst(commonRepo.WithByID(detail.AppId)) - if err != nil { - return appDetailDTO, err - } - if err := checkLimit(app); err != nil { - appDetailDTO.Enable = false - } - return appDetailDTO, nil -} -func (a AppService) GetAppDetailByID(id uint) (*response.AppDetailDTO, error) { - res := &response.AppDetailDTO{} - appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(id)) - if err != nil { - return nil, err - } - res.AppDetail = appDetail - paramMap := make(map[string]interface{}) - if err := json.Unmarshal([]byte(appDetail.Params), ¶mMap); err != nil { - return nil, err - } - res.Params = paramMap - res.HostMode = isHostModel(appDetail.DockerCompose) - return res, nil -} - -func (a AppService) GetIgnoredApp() ([]response.IgnoredApp, error) { - var res []response.IgnoredApp - details, _ := appDetailRepo.GetBy(appDetailRepo.WithIgnored()) - if len(details) == 0 { - return res, nil - } - for _, detail := range details { - app, err := appRepo.GetFirst(commonRepo.WithByID(detail.AppId)) - if err != nil { - return nil, err - } - res = append(res, response.IgnoredApp{ - Name: app.Name, - Version: detail.Version, - DetailID: detail.ID, - Icon: app.Icon, - }) - } - return res, nil -} - -func (a AppService) Install(ctx context.Context, req request.AppInstallCreate) (appInstall *model.AppInstall, err error) { - if err = docker.CreateDefaultDockerNetwork(); err != nil { - err = buserr.WithDetail(constant.Err1PanelNetworkFailed, err.Error(), nil) - return - } - if list, _ := appInstallRepo.ListBy(commonRepo.WithByName(req.Name)); len(list) > 0 { - err = buserr.New(constant.ErrAppNameExist) - return - } - var ( - httpPort int - httpsPort int - appDetail model.AppDetail - app model.App - ) - appDetail, err = appDetailRepo.GetFirst(commonRepo.WithByID(req.AppDetailId)) - if err != nil { - return - } - app, err = appRepo.GetFirst(commonRepo.WithByID(appDetail.AppId)) - if err != nil { - return - } - if DatabaseKeys[app.Key] > 0 { - if existDatabases, _ := databaseRepo.GetList(commonRepo.WithByName(req.Name)); len(existDatabases) > 0 { - err = buserr.New(constant.ErrRemoteExist) - return - } - } - for key := range req.Params { - if !strings.Contains(key, "PANEL_APP_PORT") { - continue - } - var port int - if port, err = checkPort(key, req.Params); err == nil { - if key == "PANEL_APP_PORT_HTTP" { - httpPort = port - } - if key == "PANEL_APP_PORT_HTTPS" { - httpsPort = port - } - } else { - return - } - } - - if err = checkRequiredAndLimit(app); err != nil { - return - } - - appInstall = &model.AppInstall{ - Name: req.Name, - AppId: appDetail.AppId, - AppDetailId: appDetail.ID, - Version: appDetail.Version, - Status: constant.Installing, - HttpPort: httpPort, - HttpsPort: httpsPort, - App: app, - } - composeMap := make(map[string]interface{}) - if req.EditCompose { - if err = yaml.Unmarshal([]byte(req.DockerCompose), &composeMap); err != nil { - return - } - } else { - if err = yaml.Unmarshal([]byte(appDetail.DockerCompose), &composeMap); err != nil { - return - } - } - - value, ok := composeMap["services"] - if !ok || value == nil { - err = buserr.New(constant.ErrFileParse) - return - } - servicesMap := value.(map[string]interface{}) - containerName := constant.ContainerPrefix + app.Key + "-" + common.RandStr(4) - if req.Advanced && req.ContainerName != "" { - containerName = req.ContainerName - appInstalls, _ := appInstallRepo.ListBy(appInstallRepo.WithContainerName(containerName)) - if len(appInstalls) > 0 { - err = buserr.New(constant.ErrContainerName) - return - } - containerExist := false - containerExist, err = checkContainerNameIsExist(req.ContainerName, appInstall.GetPath()) - if err != nil { - return - } - if containerExist { - err = buserr.New(constant.ErrContainerName) - return - } - } - req.Params[constant.ContainerName] = containerName - appInstall.ContainerName = containerName - - index := 0 - serviceName := "" - for k := range servicesMap { - serviceName = k - if index > 0 { - continue - } - index++ - } - if app.Limit == 0 && appInstall.Name != serviceName && len(servicesMap) == 1 { - servicesMap[appInstall.Name] = servicesMap[serviceName] - delete(servicesMap, serviceName) - serviceName = appInstall.Name - } - appInstall.ServiceName = serviceName - - if err = addDockerComposeCommonParam(composeMap, appInstall.ServiceName, req.AppContainerConfig, req.Params); err != nil { - return - } - var ( - composeByte []byte - paramByte []byte - ) - - composeByte, err = yaml.Marshal(composeMap) - if err != nil { - return - } - appInstall.DockerCompose = string(composeByte) - - defer func() { - if err != nil { - hErr := handleAppInstallErr(ctx, appInstall) - if hErr != nil { - global.LOG.Errorf("delete app dir error %s", hErr.Error()) - } - } - }() - if hostName, ok := req.Params["PANEL_DB_HOST"]; ok { - database, _ := databaseRepo.Get(commonRepo.WithByName(hostName.(string))) - if !reflect.DeepEqual(database, model.Database{}) { - req.Params["PANEL_DB_HOST"] = database.Address - req.Params["PANEL_DB_PORT"] = database.Port - req.Params["PANEL_DB_HOST_NAME"] = hostName - } - } - paramByte, err = json.Marshal(req.Params) - if err != nil { - return - } - appInstall.Env = string(paramByte) - - if err = appInstallRepo.Create(ctx, appInstall); err != nil { - return - } - if err = createLink(ctx, app, appInstall, req.Params); err != nil { - return - } - go func() { - defer func() { - if err != nil { - appInstall.Status = constant.UpErr - appInstall.Message = err.Error() - _ = appInstallRepo.Save(context.Background(), appInstall) - } - }() - if err = copyData(app, appDetail, appInstall, req); err != nil { - return - } - if err = runScript(appInstall, "init"); err != nil { - return - } - upApp(appInstall, req.PullImage) - }() - go updateToolApp(appInstall) - return -} - -func (a AppService) SyncAppListFromLocal() { - fileOp := files.NewFileOp() - localAppDir := constant.LocalAppResourceDir - if !fileOp.Stat(localAppDir) { - return - } - var ( - err error - dirEntries []os.DirEntry - localApps []model.App - ) - - defer func() { - if err != nil { - global.LOG.Errorf("Sync local app failed %v", err) - } - }() - - global.LOG.Infof("Starting local application synchronization ...") - dirEntries, err = os.ReadDir(localAppDir) - if err != nil { - return - } - for _, dirEntry := range dirEntries { - if dirEntry.IsDir() { - appDir := filepath.Join(localAppDir, dirEntry.Name()) - appDirEntries, err := os.ReadDir(appDir) - if err != nil { - global.LOG.Errorf(i18n.GetMsgWithMap("ErrAppDirNull", map[string]interface{}{"name": dirEntry.Name(), "err": err.Error()})) - continue - } - app, err := handleLocalApp(appDir) - if err != nil { - global.LOG.Errorf(i18n.GetMsgWithMap("LocalAppErr", map[string]interface{}{"name": dirEntry.Name(), "err": err.Error()})) - continue - } - var appDetails []model.AppDetail - for _, appDirEntry := range appDirEntries { - if appDirEntry.IsDir() { - appDetail := model.AppDetail{ - Version: appDirEntry.Name(), - Status: constant.AppNormal, - } - versionDir := filepath.Join(appDir, appDirEntry.Name()) - if err = handleLocalAppDetail(versionDir, &appDetail); err != nil { - global.LOG.Errorf(i18n.GetMsgWithMap("LocalAppVersionErr", map[string]interface{}{"name": app.Name, "version": appDetail.Version, "err": err.Error()})) - continue - } - appDetails = append(appDetails, appDetail) - } - } - if len(appDetails) > 0 { - app.Details = appDetails - localApps = append(localApps, *app) - } else { - global.LOG.Errorf(i18n.GetMsgWithMap("LocalAppVersionNull", map[string]interface{}{"name": app.Name})) - } - } - } - - var ( - newApps []model.App - deleteApps []model.App - updateApps []model.App - oldAppIds []uint - - deleteAppIds []uint - deleteAppDetails []model.AppDetail - newAppDetails []model.AppDetail - updateDetails []model.AppDetail - - appTags []*model.AppTag - ) - - oldApps, _ := appRepo.GetBy(appRepo.WithResource(constant.AppResourceLocal)) - apps := make(map[string]model.App, len(oldApps)) - for _, old := range oldApps { - old.Status = constant.AppTakeDown - apps[old.Key] = old - } - for _, app := range localApps { - if oldApp, ok := apps[app.Key]; ok { - app.ID = oldApp.ID - appDetails := make(map[string]model.AppDetail, len(oldApp.Details)) - for _, old := range oldApp.Details { - old.Status = constant.AppTakeDown - appDetails[old.Version] = old - } - for i, newDetail := range app.Details { - version := newDetail.Version - newDetail.Status = constant.AppNormal - newDetail.AppId = app.ID - oldDetail, exist := appDetails[version] - if exist { - newDetail.ID = oldDetail.ID - delete(appDetails, version) - } - app.Details[i] = newDetail - } - for _, v := range appDetails { - app.Details = append(app.Details, v) - } - } - app.TagsKey = append(app.TagsKey, constant.AppResourceLocal) - apps[app.Key] = app - } - - for _, app := range apps { - if app.ID == 0 { - newApps = append(newApps, app) - } else { - oldAppIds = append(oldAppIds, app.ID) - if app.Status == constant.AppTakeDown { - installs, _ := appInstallRepo.ListBy(appInstallRepo.WithAppId(app.ID)) - if len(installs) > 0 { - updateApps = append(updateApps, app) - continue - } - deleteAppIds = append(deleteAppIds, app.ID) - deleteApps = append(deleteApps, app) - deleteAppDetails = append(deleteAppDetails, app.Details...) - } else { - updateApps = append(updateApps, app) - } - } - - } - - tags, _ := tagRepo.All() - tagMap := make(map[string]uint, len(tags)) - for _, tag := range tags { - tagMap[tag.Key] = tag.ID - } - - tx, ctx := getTxAndContext() - defer tx.Rollback() - if len(newApps) > 0 { - if err = appRepo.BatchCreate(ctx, newApps); err != nil { - return - } - } - for _, update := range updateApps { - if err = appRepo.Save(ctx, &update); err != nil { - return - } - } - if len(deleteApps) > 0 { - if err = appRepo.BatchDelete(ctx, deleteApps); err != nil { - return - } - if err = appDetailRepo.DeleteByAppIds(ctx, deleteAppIds); err != nil { - return - } - } - - if err = appTagRepo.DeleteByAppIds(ctx, oldAppIds); err != nil { - return - } - for _, newApp := range newApps { - if newApp.ID > 0 { - for _, detail := range newApp.Details { - detail.AppId = newApp.ID - newAppDetails = append(newAppDetails, detail) - } - } - } - for _, update := range updateApps { - for _, detail := range update.Details { - if detail.ID == 0 { - detail.AppId = update.ID - newAppDetails = append(newAppDetails, detail) - } else { - if detail.Status == constant.AppNormal { - updateDetails = append(updateDetails, detail) - } else { - deleteAppDetails = append(deleteAppDetails, detail) - } - } - } - } - - allApps := append(newApps, updateApps...) - for _, app := range allApps { - for _, t := range app.TagsKey { - tagId, ok := tagMap[t] - if ok { - appTags = append(appTags, &model.AppTag{ - AppId: app.ID, - TagId: tagId, - }) - } - } - } - - if len(newAppDetails) > 0 { - if err = appDetailRepo.BatchCreate(ctx, newAppDetails); err != nil { - return - } - } - - for _, updateAppDetail := range updateDetails { - if err = appDetailRepo.Update(ctx, updateAppDetail); err != nil { - return - } - } - - if len(deleteAppDetails) > 0 { - if err = appDetailRepo.BatchDelete(ctx, deleteAppDetails); err != nil { - return - } - } - - if len(oldAppIds) > 0 { - if err = appTagRepo.DeleteByAppIds(ctx, oldAppIds); err != nil { - return - } - } - - if len(appTags) > 0 { - if err = appTagRepo.BatchCreate(ctx, appTags); err != nil { - return - } - } - tx.Commit() - global.LOG.Infof("Synchronization of local applications completed") -} - -func (a AppService) GetAppUpdate() (*response.AppUpdateRes, error) { - res := &response.AppUpdateRes{ - CanUpdate: false, - } - - versionUrl := fmt.Sprintf("%s/%s/1panel.json.version.txt", global.CONF.System.AppRepo, global.CONF.System.Mode) - _, versionRes, err := http2.HandleGet(versionUrl, http.MethodGet, constant.TimeOut20s) - if err != nil { - return nil, err - } - lastModifiedStr := string(versionRes) - lastModified, err := strconv.Atoi(lastModifiedStr) - if err != nil { - return nil, err - } - setting, err := NewISettingService().GetSettingInfo() - if err != nil { - return nil, err - } - if setting.AppStoreSyncStatus == constant.Syncing { - res.IsSyncing = true - return res, nil - } - - appStoreLastModified, _ := strconv.Atoi(setting.AppStoreLastModified) - res.AppStoreLastModified = appStoreLastModified - if setting.AppStoreLastModified == "" || lastModified != appStoreLastModified { - res.CanUpdate = true - return res, err - } - apps, _ := appRepo.GetBy(appRepo.WithResource(constant.AppResourceRemote)) - for _, app := range apps { - if app.Icon == "" { - res.CanUpdate = true - return res, err - } - } - - list, err := getAppList() - if err != nil { - return res, err - } - if list.Extra.Version != "" && setting.SystemVersion != list.Extra.Version && !common.CompareVersion(setting.SystemVersion, list.Extra.Version) { - global.LOG.Errorf("The current version is too low to synchronize with the App Store. The minimum required version is %s", list.Extra.Version) - return nil, buserr.New("ErrVersionTooLow") - } - res.AppList = list - return res, nil -} - -func getAppFromRepo(downloadPath string) error { - downloadUrl := downloadPath - global.LOG.Infof("[AppStore] download file from %s", downloadUrl) - fileOp := files.NewFileOp() - packagePath := filepath.Join(constant.ResourceDir, filepath.Base(downloadUrl)) - if err := fileOp.DownloadFileWithProxy(downloadUrl, packagePath); err != nil { - return err - } - if err := fileOp.Decompress(packagePath, constant.ResourceDir, files.SdkZip, ""); err != nil { - return err - } - defer func() { - _ = fileOp.DeleteFile(packagePath) - }() - return nil -} - -func getAppList() (*dto.AppList, error) { - list := &dto.AppList{} - if err := getAppFromRepo(fmt.Sprintf("%s/%s/1panel.json.zip", global.CONF.System.AppRepo, global.CONF.System.Mode)); err != nil { - return nil, err - } - listFile := filepath.Join(constant.ResourceDir, "1panel.json") - content, err := os.ReadFile(listFile) - if err != nil { - return nil, err - } - if err = json.Unmarshal(content, list); err != nil { - return nil, err - } - return list, nil -} - -var InitTypes = map[string]struct{}{ - "runtime": {}, - "php": {}, - "node": {}, -} - -func (a AppService) SyncAppListFromRemote() (err error) { - global.LOG.Infof("Starting synchronization with App Store...") - updateRes, err := a.GetAppUpdate() - if err != nil { - return err - } - if !updateRes.CanUpdate { - if updateRes.IsSyncing { - global.LOG.Infof("AppStore is Syncing!") - return - } - global.LOG.Infof("The App Store is at the latest version") - return - } - - list := &dto.AppList{} - if updateRes.AppList == nil { - list, err = getAppList() - if err != nil { - return - } - } else { - list = updateRes.AppList - } - settingService := NewISettingService() - _ = settingService.Update("AppStoreSyncStatus", constant.Syncing) - - var ( - tags []*model.Tag - appTags []*model.AppTag - oldAppIds []uint - ) - for _, t := range list.Extra.Tags { - tags = append(tags, &model.Tag{ - Key: t.Key, - Name: t.Name, - Sort: t.Sort, - }) - } - oldApps, err := appRepo.GetBy(appRepo.WithResource(constant.AppResourceRemote)) - if err != nil { - return - } - for _, old := range oldApps { - oldAppIds = append(oldAppIds, old.ID) - } - - transport := xpack.LoadRequestTransport() - baseRemoteUrl := fmt.Sprintf("%s/%s/1panel", global.CONF.System.AppRepo, global.CONF.System.Mode) - appsMap := getApps(oldApps, list.Apps) - - global.LOG.Infof("Starting synchronization of application details...") - for _, l := range list.Apps { - app := appsMap[l.AppProperty.Key] - _, iconRes, err := httpUtil.HandleGetWithTransport(l.Icon, http.MethodGet, transport, constant.TimeOut20s) - if err != nil { - return err - } - iconStr := "" - if !strings.Contains(string(iconRes), "") { - iconStr = base64.StdEncoding.EncodeToString(iconRes) - } - - app.Icon = iconStr - app.TagsKey = l.AppProperty.Tags - if l.AppProperty.Recommend > 0 { - app.Recommend = l.AppProperty.Recommend - } else { - app.Recommend = 9999 - } - app.ReadMe = l.ReadMe - app.LastModified = l.LastModified - versions := l.Versions - detailsMap := getAppDetails(app.Details, versions) - for _, v := range versions { - version := v.Name - detail := detailsMap[version] - versionUrl := fmt.Sprintf("%s/%s/%s", baseRemoteUrl, app.Key, version) - - if _, ok := InitTypes[app.Type]; ok { - dockerComposeUrl := fmt.Sprintf("%s/%s", versionUrl, "docker-compose.yml") - _, composeRes, err := httpUtil.HandleGetWithTransport(dockerComposeUrl, http.MethodGet, transport, constant.TimeOut20s) - if err != nil { - return err - } - detail.DockerCompose = string(composeRes) - } else { - detail.DockerCompose = "" - } - - paramByte, _ := json.Marshal(v.AppForm) - detail.Params = string(paramByte) - detail.DownloadUrl = fmt.Sprintf("%s/%s", versionUrl, app.Key+"-"+version+".tar.gz") - detail.DownloadCallBackUrl = v.DownloadCallBackUrl - detail.Update = true - detail.LastModified = v.LastModified - detailsMap[version] = detail - } - var newDetails []model.AppDetail - for _, detail := range detailsMap { - newDetails = append(newDetails, detail) - } - app.Details = newDetails - appsMap[l.AppProperty.Key] = app - } - - global.LOG.Infof("Synchronization of application details Success") - - var ( - addAppArray []model.App - updateAppArray []model.App - deleteAppArray []model.App - deleteIds []uint - tagMap = make(map[string]uint, len(tags)) - ) - - for _, v := range appsMap { - if v.ID == 0 { - addAppArray = append(addAppArray, v) - } else { - if v.Status == constant.AppTakeDown { - installs, _ := appInstallRepo.ListBy(appInstallRepo.WithAppId(v.ID)) - if len(installs) > 0 { - updateAppArray = append(updateAppArray, v) - continue - } - deleteAppArray = append(deleteAppArray, v) - deleteIds = append(deleteIds, v.ID) - } else { - updateAppArray = append(updateAppArray, v) - } - } - } - tx, ctx := getTxAndContext() - defer tx.Rollback() - if len(addAppArray) > 0 { - if err = appRepo.BatchCreate(ctx, addAppArray); err != nil { - return - } - } - if len(deleteAppArray) > 0 { - if err = appRepo.BatchDelete(ctx, deleteAppArray); err != nil { - return - } - if err = appDetailRepo.DeleteByAppIds(ctx, deleteIds); err != nil { - return - } - } - if err = tagRepo.DeleteAll(ctx); err != nil { - return - } - if len(tags) > 0 { - if err = tagRepo.BatchCreate(ctx, tags); err != nil { - return - } - for _, t := range tags { - tagMap[t.Key] = t.ID - } - } - for _, update := range updateAppArray { - if err = appRepo.Save(ctx, &update); err != nil { - return - } - } - apps := append(addAppArray, updateAppArray...) - - var ( - addDetails []model.AppDetail - updateDetails []model.AppDetail - deleteDetails []model.AppDetail - ) - for _, app := range apps { - for _, t := range app.TagsKey { - tagId, ok := tagMap[t] - if ok { - appTags = append(appTags, &model.AppTag{ - AppId: app.ID, - TagId: tagId, - }) - } - } - for _, d := range app.Details { - d.AppId = app.ID - if d.ID == 0 { - addDetails = append(addDetails, d) - } else { - if d.Status == constant.AppTakeDown { - runtime, _ := runtimeRepo.GetFirst(runtimeRepo.WithDetailId(d.ID)) - if runtime != nil { - updateDetails = append(updateDetails, d) - continue - } - installs, _ := appInstallRepo.ListBy(appInstallRepo.WithDetailIdsIn([]uint{d.ID})) - if len(installs) > 0 { - updateDetails = append(updateDetails, d) - continue - } - deleteDetails = append(deleteDetails, d) - } else { - updateDetails = append(updateDetails, d) - } - } - } - } - if len(addDetails) > 0 { - if err = appDetailRepo.BatchCreate(ctx, addDetails); err != nil { - return - } - } - if len(deleteDetails) > 0 { - if err = appDetailRepo.BatchDelete(ctx, deleteDetails); err != nil { - return - } - } - for _, u := range updateDetails { - if err = appDetailRepo.Update(ctx, u); err != nil { - return - } - } - - if len(oldAppIds) > 0 { - if err = appTagRepo.DeleteByAppIds(ctx, oldAppIds); err != nil { - return - } - } - - if len(appTags) > 0 { - if err = appTagRepo.BatchCreate(ctx, appTags); err != nil { - return - } - } - tx.Commit() - - _ = settingService.Update("AppStoreSyncStatus", constant.SyncSuccess) - _ = settingService.Update("AppStoreLastModified", strconv.Itoa(list.LastModified)) - - global.LOG.Infof("Synchronization with the App Store was successful!") - return -} diff --git a/backend/app/service/app_install.go b/backend/app/service/app_install.go deleted file mode 100644 index 41fbfe9f1..000000000 --- a/backend/app/service/app_install.go +++ /dev/null @@ -1,848 +0,0 @@ -package service - -import ( - "context" - "encoding/json" - "fmt" - "math" - "net/http" - "os" - "path" - "path/filepath" - "reflect" - "sort" - "strconv" - "strings" - - "github.com/1Panel-dev/1Panel/backend/utils/files" - httpUtil "github.com/1Panel-dev/1Panel/backend/utils/http" - "github.com/docker/docker/api/types" - "gopkg.in/yaml.v3" - - "github.com/1Panel-dev/1Panel/backend/utils/env" - "github.com/1Panel-dev/1Panel/backend/utils/nginx" - "github.com/joho/godotenv" - - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/app/dto/response" - "github.com/1Panel-dev/1Panel/backend/buserr" - - "github.com/1Panel-dev/1Panel/backend/app/repo" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/1Panel-dev/1Panel/backend/utils/compose" - "github.com/1Panel-dev/1Panel/backend/utils/docker" - "github.com/pkg/errors" -) - -type AppInstallService struct { -} - -type IAppInstallService interface { - Page(req request.AppInstalledSearch) (int64, []response.AppInstallDTO, error) - CheckExist(req request.AppInstalledInfo) (*response.AppInstalledCheck, error) - LoadPort(req dto.OperationWithNameAndType) (int64, error) - LoadConnInfo(req dto.OperationWithNameAndType) (response.DatabaseConn, error) - SearchForWebsite(req request.AppInstalledSearch) ([]response.AppInstallDTO, error) - Operate(req request.AppInstalledOperate) error - Update(req request.AppInstalledUpdate) error - IgnoreUpgrade(req request.AppInstalledIgnoreUpgrade) error - SyncAll(systemInit bool) error - GetServices(key string) ([]response.AppService, error) - GetUpdateVersions(req request.AppUpdateVersion) ([]dto.AppVersion, error) - GetParams(id uint) (*response.AppConfig, error) - ChangeAppPort(req request.PortUpdate) error - GetDefaultConfigByKey(key, name string) (string, error) - DeleteCheck(installId uint) ([]dto.AppResource, error) - - GetInstallList() ([]dto.AppInstallInfo, error) -} - -func NewIAppInstalledService() IAppInstallService { - return &AppInstallService{} -} - -func (a *AppInstallService) GetInstallList() ([]dto.AppInstallInfo, error) { - var datas []dto.AppInstallInfo - appInstalls, err := appInstallRepo.ListBy() - if err != nil { - return nil, err - } - for _, install := range appInstalls { - datas = append(datas, dto.AppInstallInfo{ID: install.ID, Key: install.App.Key, Name: install.Name}) - } - return datas, nil -} - -func (a *AppInstallService) Page(req request.AppInstalledSearch) (int64, []response.AppInstallDTO, error) { - var ( - opts []repo.DBOption - total int64 - installs []model.AppInstall - err error - ) - - if req.Name != "" { - opts = append(opts, commonRepo.WithLikeName(req.Name)) - } - if len(req.Tags) != 0 { - tags, err := tagRepo.GetByKeys(req.Tags) - if err != nil { - return 0, nil, err - } - var tagIds []uint - for _, t := range tags { - tagIds = append(tagIds, t.ID) - } - appTags, err := appTagRepo.GetByTagIds(tagIds) - if err != nil { - return 0, nil, err - } - var appIds []uint - for _, t := range appTags { - appIds = append(appIds, t.AppId) - } - - opts = append(opts, appInstallRepo.WithAppIdsIn(appIds)) - } - - if req.Update { - installs, err = appInstallRepo.ListBy(opts...) - if err != nil { - return 0, nil, err - } - } else { - total, installs, err = appInstallRepo.Page(req.Page, req.PageSize, opts...) - if err != nil { - return 0, nil, err - } - } - - installDTOs, err := handleInstalled(installs, req.Update, req.Sync) - if err != nil { - return 0, nil, err - } - if req.Update { - total = int64(len(installDTOs)) - } - - return total, installDTOs, nil -} - -func (a *AppInstallService) CheckExist(req request.AppInstalledInfo) (*response.AppInstalledCheck, error) { - res := &response.AppInstalledCheck{ - IsExist: false, - } - - app, err := appRepo.GetFirst(appRepo.WithKey(req.Key)) - if err != nil { - return res, nil - } - res.App = app.Name - - var appInstall model.AppInstall - if len(req.Name) == 0 { - appInstall, _ = appInstallRepo.GetFirst(appInstallRepo.WithAppId(app.ID)) - } else { - appInstall, _ = appInstallRepo.GetFirst(appInstallRepo.WithAppId(app.ID), commonRepo.WithByName(req.Name)) - } - - if reflect.DeepEqual(appInstall, model.AppInstall{}) { - return res, nil - } - if err = syncAppInstallStatus(&appInstall, false); err != nil { - return nil, err - } - - res.ContainerName = appInstall.ContainerName - res.Name = appInstall.Name - res.Version = appInstall.Version - res.CreatedAt = appInstall.CreatedAt - res.Status = appInstall.Status - res.AppInstallID = appInstall.ID - res.IsExist = true - res.InstallPath = path.Join(constant.AppInstallDir, appInstall.App.Key, appInstall.Name) - res.HttpPort = appInstall.HttpPort - res.HttpsPort = appInstall.HttpsPort - - return res, nil -} - -func (a *AppInstallService) LoadPort(req dto.OperationWithNameAndType) (int64, error) { - app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name) - if err != nil { - return int64(0), nil - } - return app.Port, nil -} - -func (a *AppInstallService) LoadConnInfo(req dto.OperationWithNameAndType) (response.DatabaseConn, error) { - var data response.DatabaseConn - app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name) - if err != nil { - return data, nil - } - data.Status = app.Status - data.Username = app.UserName - data.Password = app.Password - data.ServiceName = app.ServiceName - data.Port = app.Port - data.ContainerName = app.ContainerName - return data, nil -} - -func (a *AppInstallService) SearchForWebsite(req request.AppInstalledSearch) ([]response.AppInstallDTO, error) { - var ( - installs []model.AppInstall - err error - opts []repo.DBOption - ) - if req.Type != "" { - apps, err := appRepo.GetBy(appRepo.WithType(req.Type)) - if err != nil { - return nil, err - } - var ids []uint - for _, app := range apps { - ids = append(ids, app.ID) - } - if req.Unused { - opts = append(opts, appInstallRepo.WithIdNotInWebsite()) - } - opts = append(opts, appInstallRepo.WithAppIdsIn(ids)) - installs, err = appInstallRepo.ListBy(opts...) - if err != nil { - return nil, err - } - } else { - installs, err = appInstallRepo.ListBy() - if err != nil { - return nil, err - } - } - - return handleInstalled(installs, false, true) -} - -func (a *AppInstallService) Operate(req request.AppInstalledOperate) error { - install, err := appInstallRepo.GetFirstByCtx(context.Background(), commonRepo.WithByID(req.InstallId)) - if err != nil { - return err - } - if !req.ForceDelete && !files.NewFileOp().Stat(install.GetPath()) { - return buserr.New(constant.ErrInstallDirNotFound) - } - dockerComposePath := install.GetComposePath() - switch req.Operate { - case constant.Rebuild: - return rebuildApp(install) - case constant.Start: - out, err := compose.Start(dockerComposePath) - if err != nil { - return handleErr(install, err, out) - } - return syncAppInstallStatus(&install, false) - case constant.Stop: - out, err := compose.Stop(dockerComposePath) - if err != nil { - return handleErr(install, err, out) - } - return syncAppInstallStatus(&install, false) - case constant.Restart: - out, err := compose.Restart(dockerComposePath) - if err != nil { - return handleErr(install, err, out) - } - return syncAppInstallStatus(&install, false) - case constant.Delete: - if err := deleteAppInstall(install, req.DeleteBackup, req.ForceDelete, req.DeleteDB); err != nil && !req.ForceDelete { - return err - } - return nil - case constant.Sync: - return syncAppInstallStatus(&install, true) - case constant.Upgrade: - upgradeReq := request.AppInstallUpgrade{ - InstallID: install.ID, - DetailID: req.DetailId, - Backup: req.Backup, - PullImage: req.PullImage, - DockerCompose: req.DockerCompose, - } - return upgradeInstall(upgradeReq) - case constant.Reload: - return opNginx(install.ContainerName, constant.NginxReload) - default: - return errors.New("operate not support") - } -} - -func (a *AppInstallService) Update(req request.AppInstalledUpdate) error { - installed, err := appInstallRepo.GetFirst(commonRepo.WithByID(req.InstallId)) - if err != nil { - return err - } - changePort := false - port, ok := req.Params["PANEL_APP_PORT_HTTP"] - if ok { - portN := int(math.Ceil(port.(float64))) - if portN != installed.HttpPort { - changePort = true - httpPort, err := checkPort("PANEL_APP_PORT_HTTP", req.Params) - if err != nil { - return err - } - installed.HttpPort = httpPort - } - } - ports, ok := req.Params["PANEL_APP_PORT_HTTPS"] - if ok { - portN := int(math.Ceil(ports.(float64))) - if portN != installed.HttpsPort { - httpsPort, err := checkPort("PANEL_APP_PORT_HTTPS", req.Params) - if err != nil { - return err - } - installed.HttpsPort = httpsPort - } - } - - backupDockerCompose := installed.DockerCompose - if req.Advanced { - composeMap := make(map[string]interface{}) - if req.EditCompose { - if err = yaml.Unmarshal([]byte(req.DockerCompose), &composeMap); err != nil { - return err - } - } else { - if err = yaml.Unmarshal([]byte(installed.DockerCompose), &composeMap); err != nil { - return err - } - } - if err = addDockerComposeCommonParam(composeMap, installed.ServiceName, req.AppContainerConfig, req.Params); err != nil { - return err - } - composeByte, err := yaml.Marshal(composeMap) - if err != nil { - return err - } - installed.DockerCompose = string(composeByte) - if req.ContainerName == "" { - req.Params[constant.ContainerName] = installed.ContainerName - } else { - req.Params[constant.ContainerName] = req.ContainerName - if installed.ContainerName != req.ContainerName { - exist, _ := appInstallRepo.GetFirst(appInstallRepo.WithContainerName(req.ContainerName), appInstallRepo.WithIDNotIs(installed.ID)) - if exist.ID > 0 { - return buserr.New(constant.ErrContainerName) - } - containerExist, err := checkContainerNameIsExist(req.ContainerName, installed.GetPath()) - if err != nil { - return err - } - if containerExist { - return buserr.New(constant.ErrContainerName) - } - installed.ContainerName = req.ContainerName - } - } - } - - envPath := path.Join(installed.GetPath(), ".env") - oldEnvMaps, err := godotenv.Read(envPath) - if err != nil { - return err - } - backupEnvMaps := oldEnvMaps - handleMap(req.Params, oldEnvMaps) - paramByte, err := json.Marshal(oldEnvMaps) - if err != nil { - return err - } - installed.Env = string(paramByte) - if err := env.Write(oldEnvMaps, envPath); err != nil { - return err - } - fileOp := files.NewFileOp() - _ = fileOp.WriteFile(installed.GetComposePath(), strings.NewReader(installed.DockerCompose), 0755) - if err := rebuildApp(installed); err != nil { - _ = env.Write(backupEnvMaps, envPath) - _ = fileOp.WriteFile(installed.GetComposePath(), strings.NewReader(backupDockerCompose), 0755) - return err - } - installed.Status = constant.Running - _ = appInstallRepo.Save(context.Background(), &installed) - - website, _ := websiteRepo.GetFirst(websiteRepo.WithAppInstallId(installed.ID)) - if changePort && website.ID != 0 && website.Status == constant.Running { - go func() { - nginxInstall, err := getNginxFull(&website) - if err != nil { - global.LOG.Errorf(buserr.WithErr(constant.ErrUpdateBuWebsite, err).Error()) - return - } - config := nginxInstall.SiteConfig.Config - servers := config.FindServers() - if len(servers) == 0 { - global.LOG.Errorf(buserr.WithErr(constant.ErrUpdateBuWebsite, errors.New("nginx config is not valid")).Error()) - return - } - server := servers[0] - proxy := fmt.Sprintf("http://127.0.0.1:%d", installed.HttpPort) - server.UpdateRootProxy([]string{proxy}) - - if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { - global.LOG.Errorf(buserr.WithErr(constant.ErrUpdateBuWebsite, err).Error()) - return - } - if err := nginxCheckAndReload(nginxInstall.SiteConfig.OldContent, config.FilePath, nginxInstall.Install.ContainerName); err != nil { - global.LOG.Errorf(buserr.WithErr(constant.ErrUpdateBuWebsite, err).Error()) - return - } - }() - } - return nil -} - -func (a *AppInstallService) IgnoreUpgrade(req request.AppInstalledIgnoreUpgrade) error { - appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(req.DetailID)) - if err != nil { - return err - } - appDetail.IgnoreUpgrade = req.Operate == "ignore" - return appDetailRepo.Update(context.Background(), appDetail) -} - -func (a *AppInstallService) SyncAll(systemInit bool) error { - allList, err := appInstallRepo.ListBy() - if err != nil { - return err - } - for _, i := range allList { - if i.Status == constant.Installing || i.Status == constant.Upgrading || i.Status == constant.Rebuilding { - if systemInit { - i.Status = constant.Error - i.Message = "1Panel restart causes the task to terminate" - _ = appInstallRepo.Save(context.Background(), &i) - } - continue - } - if !systemInit { - if err = syncAppInstallStatus(&i, false); err != nil { - global.LOG.Errorf("sync install app[%s] error,mgs: %s", i.Name, err.Error()) - } - } - } - return nil -} - -func (a *AppInstallService) GetServices(key string) ([]response.AppService, error) { - var res []response.AppService - if DatabaseKeys[key] > 0 { - if key == constant.AppPostgres { - key = constant.AppPostgresql - } - dbs, _ := databaseRepo.GetList(commonRepo.WithByType(key)) - if len(dbs) == 0 { - return res, nil - } - for _, db := range dbs { - service := response.AppService{ - Label: db.Name, - Value: db.Name, - } - if db.AppInstallID > 0 { - install, err := appInstallRepo.GetFirst(commonRepo.WithByID(db.AppInstallID)) - if err != nil { - return nil, err - } - paramMap := make(map[string]string) - if install.Param != "" { - _ = json.Unmarshal([]byte(install.Param), ¶mMap) - } - service.Config = paramMap - service.From = constant.AppResourceLocal - } else { - service.From = constant.AppResourceRemote - } - res = append(res, service) - } - } else { - app, err := appRepo.GetFirst(appRepo.WithKey(key)) - if err != nil { - return nil, err - } - installs, err := appInstallRepo.ListBy(appInstallRepo.WithAppId(app.ID), appInstallRepo.WithStatus(constant.Running)) - if err != nil { - return nil, err - } - for _, install := range installs { - paramMap := make(map[string]string) - if install.Param != "" { - _ = json.Unmarshal([]byte(install.Param), ¶mMap) - } - res = append(res, response.AppService{ - Label: install.Name, - Value: install.ServiceName, - Config: paramMap, - }) - } - } - return res, nil -} - -func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]dto.AppVersion, error) { - install, err := appInstallRepo.GetFirst(commonRepo.WithByID(req.AppInstallID)) - var versions []dto.AppVersion - if err != nil { - return versions, err - } - app, err := appRepo.GetFirst(commonRepo.WithByID(install.AppId)) - if err != nil { - return versions, err - } - details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(app.ID)) - if err != nil { - return versions, err - } - for _, detail := range details { - if detail.IgnoreUpgrade { - continue - } - if common.IsCrossVersion(install.Version, detail.Version) && !app.CrossVersionUpdate { - continue - } - if common.CompareVersion(detail.Version, install.Version) { - var newCompose string - if req.UpdateVersion != "" && req.UpdateVersion == detail.Version && detail.DockerCompose == "" && !app.IsLocalApp() { - filename := filepath.Base(detail.DownloadUrl) - dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(detail.DownloadUrl, filename), "docker-compose.yml") - statusCode, composeRes, err := httpUtil.HandleGet(dockerComposeUrl, http.MethodGet, constant.TimeOut20s) - if err != nil { - return versions, err - } - if statusCode > 200 { - return versions, err - } - detail.DockerCompose = string(composeRes) - _ = appDetailRepo.Update(context.Background(), detail) - } - newCompose, err = getUpgradeCompose(install, detail) - if err != nil { - return versions, err - } - versions = append(versions, dto.AppVersion{ - Version: detail.Version, - DetailId: detail.ID, - DockerCompose: newCompose, - }) - } - } - sort.Slice(versions, func(i, j int) bool { - return common.CompareVersion(versions[i].Version, versions[j].Version) - }) - return versions, nil -} - -func (a *AppInstallService) ChangeAppPort(req request.PortUpdate) error { - if common.ScanPort(int(req.Port)) { - return buserr.WithDetail(constant.ErrPortInUsed, req.Port, nil) - } - - appInstall, err := appInstallRepo.LoadBaseInfo(req.Key, req.Name) - if err != nil { - return nil - } - - if err := updateInstallInfoInDB(req.Key, req.Name, "port", strconv.FormatInt(req.Port, 10)); err != nil { - return nil - } - - appRess, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(appInstall.ID)) - for _, appRes := range appRess { - appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(appRes.AppInstallId)) - if err != nil { - return err - } - if _, err := compose.Restart(fmt.Sprintf("%s/%s/%s/docker-compose.yml", constant.AppInstallDir, appInstall.App.Key, appInstall.Name)); err != nil { - global.LOG.Errorf("docker-compose restart %s[%s] failed, err: %v", appInstall.App.Key, appInstall.Name, err) - } - } - - return nil -} - -func (a *AppInstallService) DeleteCheck(installID uint) ([]dto.AppResource, error) { - var res []dto.AppResource - appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(installID)) - if err != nil { - return nil, err - } - app, err := appRepo.GetFirst(commonRepo.WithByID(appInstall.AppId)) - if err != nil { - return nil, err - } - websites, _ := websiteRepo.GetBy(websiteRepo.WithAppInstallId(appInstall.ID)) - for _, website := range websites { - res = append(res, dto.AppResource{ - Type: "website", - Name: website.PrimaryDomain, - }) - } - if app.Key == constant.AppOpenresty { - websites, _ := websiteRepo.GetBy() - for _, website := range websites { - res = append(res, dto.AppResource{ - Type: "website", - Name: website.PrimaryDomain, - }) - } - } - if app.Type == constant.Runtime { - resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(appInstall.ID), commonRepo.WithByFrom(constant.AppResourceLocal)) - for _, resource := range resources { - linkInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(resource.AppInstallId)) - res = append(res, dto.AppResource{ - Type: "app", - Name: linkInstall.Name, - }) - } - } - return res, nil -} - -func (a *AppInstallService) GetDefaultConfigByKey(key, name string) (string, error) { - baseInfo, err := appInstallRepo.LoadBaseInfo(key, name) - if err != nil { - return "", err - } - - fileOp := files.NewFileOp() - filePath := path.Join(constant.AppResourceDir, "remote", baseInfo.Key, baseInfo.Version, "conf") - if !fileOp.Stat(filePath) { - filePath = path.Join(constant.AppResourceDir, baseInfo.Key, "versions", baseInfo.Version, "conf") - } - if !fileOp.Stat(filePath) { - return "", buserr.New(constant.ErrPathNotFound) - } - - if key == constant.AppMysql || key == constant.AppMariaDB { - filePath = path.Join(filePath, "my.cnf") - } - if key == constant.AppRedis { - filePath = path.Join(filePath, "redis.conf") - } - if key == constant.AppOpenresty { - filePath = path.Join(filePath, "nginx.conf") - } - contentByte, err := os.ReadFile(filePath) - if err != nil { - return "", err - } - return string(contentByte), nil -} - -func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) { - var ( - params []response.AppParam - appForm dto.AppForm - envs = make(map[string]interface{}) - res response.AppConfig - ) - install, err := appInstallRepo.GetFirst(commonRepo.WithByID(id)) - if err != nil { - return nil, err - } - detail, err := appDetailRepo.GetFirst(commonRepo.WithByID(install.AppDetailId)) - if err != nil { - return nil, err - } - if err = json.Unmarshal([]byte(detail.Params), &appForm); err != nil { - return nil, err - } - if err = json.Unmarshal([]byte(install.Env), &envs); err != nil { - return nil, err - } - for _, form := range appForm.FormFields { - if v, ok := envs[form.EnvKey]; ok { - appParam := response.AppParam{ - Edit: false, - Key: form.EnvKey, - Rule: form.Rule, - Type: form.Type, - Multiple: form.Multiple, - } - if form.Edit { - appParam.Edit = true - } - appParam.LabelZh = form.LabelZh - appParam.LabelEn = form.LabelEn - appParam.Value = v - if form.Type == "service" { - appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithServiceName(v.(string))) - appParam.ShowValue = appInstall.Name - } else if form.Type == "select" { - if form.Multiple { - if v == "" { - appParam.Value = []string{} - } else { - if str, ok := v.(string); ok { - appParam.Value = strings.Split(str, ",") - } - } - } else { - for _, fv := range form.Values { - if fv.Value == v { - appParam.ShowValue = fv.Label - break - } - } - } - appParam.Values = form.Values - } - params = append(params, appParam) - } else { - params = append(params, response.AppParam{ - Edit: form.Edit, - Key: form.EnvKey, - Rule: form.Rule, - Type: form.Type, - LabelZh: form.LabelZh, - LabelEn: form.LabelEn, - Value: form.Default, - Values: form.Values, - Multiple: form.Multiple, - }) - } - } - - config := getAppCommonConfig(envs) - config.DockerCompose = install.DockerCompose - res.Params = params - if config.ContainerName == "" { - config.ContainerName = install.ContainerName - } - res.AppContainerConfig = config - res.HostMode = isHostModel(install.DockerCompose) - return &res, nil -} - -func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error { - if appInstall.Status == constant.Installing || appInstall.Status == constant.Rebuilding || appInstall.Status == constant.Upgrading { - return nil - } - cli, err := docker.NewClient() - if err != nil { - return err - } - defer cli.Close() - - var ( - containers []types.Container - containersMap map[string]types.Container - containerNames = strings.Split(appInstall.ContainerName, ",") - ) - containers, err = cli.ListContainersByName(containerNames) - if err != nil { - return err - } - containersMap = make(map[string]types.Container) - for _, con := range containers { - containersMap[con.Names[0]] = con - } - synAppInstall(containersMap, appInstall, force) - return nil -} - -func updateInstallInfoInDB(appKey, appName, param string, value interface{}) error { - if param != "password" && param != "port" && param != "user-password" { - return nil - } - appInstall, err := appInstallRepo.LoadBaseInfo(appKey, appName) - if err != nil { - return nil - } - envPath := fmt.Sprintf("%s/%s/.env", appInstall.AppPath, appInstall.Name) - lineBytes, err := os.ReadFile(envPath) - if err != nil { - return err - } - - envKey := "" - switch param { - case "password": - if appKey == "mysql" || appKey == "mariadb" || appKey == "postgresql" { - envKey = "PANEL_DB_ROOT_PASSWORD=" - } else { - envKey = "PANEL_REDIS_ROOT_PASSWORD=" - } - case "port": - envKey = "PANEL_APP_PORT_HTTP=" - default: - envKey = "PANEL_DB_USER_PASSWORD=" - } - files := strings.Split(string(lineBytes), "\n") - var newFiles []string - for _, line := range files { - if strings.HasPrefix(line, envKey) { - newFiles = append(newFiles, fmt.Sprintf("%s%v", envKey, value)) - } else { - newFiles = append(newFiles, line) - } - } - file, err := os.OpenFile(envPath, os.O_WRONLY|os.O_TRUNC, 0666) - if err != nil { - return err - } - defer file.Close() - _, err = file.WriteString(strings.Join(newFiles, "\n")) - if err != nil { - return err - } - - oldVal, newVal := "", "" - if param == "password" { - oldVal = fmt.Sprintf("\"PANEL_DB_ROOT_PASSWORD\":\"%v\"", appInstall.Password) - newVal = fmt.Sprintf("\"PANEL_DB_ROOT_PASSWORD\":\"%v\"", value) - if appKey == "redis" { - oldVal = fmt.Sprintf("\"PANEL_REDIS_ROOT_PASSWORD\":\"%v\"", appInstall.Password) - newVal = fmt.Sprintf("\"PANEL_REDIS_ROOT_PASSWORD\":\"%v\"", value) - } - _ = appInstallRepo.BatchUpdateBy(map[string]interface{}{ - "param": strings.ReplaceAll(appInstall.Param, oldVal, newVal), - "env": strings.ReplaceAll(appInstall.Env, oldVal, newVal), - }, commonRepo.WithByID(appInstall.ID)) - } - if param == "user-password" { - oldVal = fmt.Sprintf("\"PANEL_DB_USER_PASSWORD\":\"%v\"", appInstall.UserPassword) - newVal = fmt.Sprintf("\"PANEL_DB_USER_PASSWORD\":\"%v\"", value) - _ = appInstallRepo.BatchUpdateBy(map[string]interface{}{ - "param": strings.ReplaceAll(appInstall.Param, oldVal, newVal), - "env": strings.ReplaceAll(appInstall.Env, oldVal, newVal), - }, commonRepo.WithByID(appInstall.ID)) - } - if param == "port" { - oldVal = fmt.Sprintf("\"PANEL_APP_PORT_HTTP\":%v", appInstall.Port) - newVal = fmt.Sprintf("\"PANEL_APP_PORT_HTTP\":%v", value) - _ = appInstallRepo.BatchUpdateBy(map[string]interface{}{ - "param": strings.ReplaceAll(appInstall.Param, oldVal, newVal), - "env": strings.ReplaceAll(appInstall.Env, oldVal, newVal), - "http_port": value, - }, commonRepo.WithByID(appInstall.ID)) - } - - ComposeFile := fmt.Sprintf("%s/%s/%s/docker-compose.yml", constant.AppInstallDir, appKey, appInstall.Name) - stdout, err := compose.Down(ComposeFile) - if err != nil { - return errors.New(stdout) - } - stdout, err = compose.Up(ComposeFile) - if err != nil { - return errors.New(stdout) - } - return nil -} diff --git a/backend/app/service/app_utils.go b/backend/app/service/app_utils.go deleted file mode 100644 index 80c740109..000000000 --- a/backend/app/service/app_utils.go +++ /dev/null @@ -1,1521 +0,0 @@ -package service - -import ( - "context" - "encoding/base64" - "encoding/json" - "fmt" - "math" - "net/http" - "os" - "os/exec" - "path" - "reflect" - "regexp" - "strconv" - "strings" - "time" - - "github.com/docker/docker/api/types" - - httpUtil "github.com/1Panel-dev/1Panel/backend/utils/http" - "github.com/1Panel-dev/1Panel/backend/utils/xpack" - "github.com/docker/docker/api/types/container" - - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/i18n" - "github.com/subosito/gotenv" - "gopkg.in/yaml.v3" - - "github.com/1Panel-dev/1Panel/backend/app/repo" - "github.com/1Panel-dev/1Panel/backend/utils/env" - - "github.com/1Panel-dev/1Panel/backend/app/dto/response" - "github.com/1Panel-dev/1Panel/backend/buserr" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/1Panel-dev/1Panel/backend/utils/compose" - "github.com/1Panel-dev/1Panel/backend/utils/docker" - composeV2 "github.com/1Panel-dev/1Panel/backend/utils/docker" - "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/pkg/errors" -) - -type DatabaseOp string - -var ( - Add DatabaseOp = "add" - Delete DatabaseOp = "delete" -) - -func checkPort(key string, params map[string]interface{}) (int, error) { - port, ok := params[key] - if ok { - portN := 0 - var err error - switch p := port.(type) { - case string: - portN, err = strconv.Atoi(p) - if err != nil { - return portN, nil - } - case float64: - portN = int(math.Ceil(p)) - case int: - portN = p - } - - oldInstalled, _ := appInstallRepo.ListBy(appInstallRepo.WithPort(portN)) - if len(oldInstalled) > 0 { - var apps []string - for _, install := range oldInstalled { - apps = append(apps, install.App.Name) - } - return portN, buserr.WithMap(constant.ErrPortInOtherApp, map[string]interface{}{"port": portN, "apps": apps}, nil) - } - if common.ScanPort(portN) { - return portN, buserr.WithDetail(constant.ErrPortInUsed, portN, nil) - } else { - return portN, nil - } - } - return 0, nil -} - -func checkPortExist(port int) error { - errMap := make(map[string]interface{}) - errMap["port"] = port - appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithPort(port)) - if appInstall.ID > 0 { - errMap["type"] = i18n.GetMsgByKey("TYPE_APP") - errMap["name"] = appInstall.Name - return buserr.WithMap("ErrPortExist", errMap, nil) - } - runtime, _ := runtimeRepo.GetFirst(runtimeRepo.WithPort(port)) - if runtime != nil { - errMap["type"] = i18n.GetMsgByKey("TYPE_RUNTIME") - errMap["name"] = runtime.Name - return buserr.WithMap("ErrPortExist", errMap, nil) - } - domain, _ := websiteDomainRepo.GetFirst(websiteDomainRepo.WithPort(port)) - if domain.ID > 0 { - errMap["type"] = i18n.GetMsgByKey("TYPE_DOMAIN") - errMap["name"] = domain.Domain - return buserr.WithMap("ErrPortExist", errMap, nil) - } - if common.ScanPort(port) { - return buserr.WithDetail(constant.ErrPortInUsed, port, nil) - } - return nil -} - -var DatabaseKeys = map[string]uint{ - constant.AppMysql: 3306, - constant.AppMariaDB: 3306, - constant.AppPostgresql: 5432, - constant.AppPostgres: 5432, - constant.AppMongodb: 27017, - constant.AppRedis: 6379, - constant.AppMemcached: 11211, -} - -var ToolKeys = map[string]uint{ - "minio": 9001, -} - -func createLink(ctx context.Context, app model.App, appInstall *model.AppInstall, params map[string]interface{}) error { - var dbConfig dto.AppDatabase - if DatabaseKeys[app.Key] > 0 { - database := &model.Database{ - AppInstallID: appInstall.ID, - Name: appInstall.Name, - Type: app.Key, - Version: appInstall.Version, - From: "local", - Address: appInstall.ServiceName, - Port: DatabaseKeys[app.Key], - } - detail, err := appDetailRepo.GetFirst(commonRepo.WithByID(appInstall.AppDetailId)) - if err != nil { - return err - } - - formFields := &dto.AppForm{} - if err := json.Unmarshal([]byte(detail.Params), formFields); err != nil { - return err - } - for _, form := range formFields.FormFields { - if form.EnvKey == "PANEL_APP_PORT_HTTP" { - portFloat, ok := form.Default.(float64) - if ok { - database.Port = uint(int(portFloat)) - } - break - } - } - - switch app.Key { - case constant.AppMysql, constant.AppMariaDB, constant.AppPostgresql, constant.AppMongodb: - if password, ok := params["PANEL_DB_ROOT_PASSWORD"]; ok { - if password != "" { - database.Password = password.(string) - if app.Key == "mysql" || app.Key == "mariadb" { - database.Username = "root" - } - if rootUser, ok := params["PANEL_DB_ROOT_USER"]; ok { - database.Username = rootUser.(string) - } - authParam := dto.AuthParam{ - RootPassword: password.(string), - RootUser: database.Username, - } - authByte, err := json.Marshal(authParam) - if err != nil { - return err - } - appInstall.Param = string(authByte) - - } - } - case constant.AppRedis: - if password, ok := params["PANEL_REDIS_ROOT_PASSWORD"]; ok { - if password != "" { - authParam := dto.RedisAuthParam{ - RootPassword: password.(string), - } - authByte, err := json.Marshal(authParam) - if err != nil { - return err - } - appInstall.Param = string(authByte) - } - database.Password = password.(string) - } - } - if err := databaseRepo.Create(ctx, database); err != nil { - return err - } - } - if ToolKeys[app.Key] > 0 { - if app.Key == "minio" { - authParam := dto.MinioAuthParam{} - if password, ok := params["PANEL_MINIO_ROOT_PASSWORD"]; ok { - authParam.RootPassword = password.(string) - } - if rootUser, ok := params["PANEL_MINIO_ROOT_USER"]; ok { - authParam.RootUser = rootUser.(string) - } - authByte, err := json.Marshal(authParam) - if err != nil { - return err - } - appInstall.Param = string(authByte) - } - } - - if app.Type == "website" || app.Type == "tool" { - paramByte, err := json.Marshal(params) - if err != nil { - return err - } - if err = json.Unmarshal(paramByte, &dbConfig); err != nil { - return err - } - } - - if !reflect.DeepEqual(dbConfig, dto.AppDatabase{}) && dbConfig.ServiceName != "" { - hostName := params["PANEL_DB_HOST_NAME"] - if hostName == nil || hostName.(string) == "" { - return nil - } - database, _ := databaseRepo.Get(commonRepo.WithByName(hostName.(string))) - if database.ID == 0 { - return nil - } - var resourceId uint - if dbConfig.DbName != "" && dbConfig.DbUser != "" && dbConfig.Password != "" { - switch database.Type { - case constant.AppPostgresql, constant.AppPostgres: - iPostgresqlRepo := repo.NewIPostgresqlRepo() - oldPostgresqlDb, _ := iPostgresqlRepo.Get(commonRepo.WithByName(dbConfig.DbName), iPostgresqlRepo.WithByFrom(constant.ResourceLocal)) - resourceId = oldPostgresqlDb.ID - if oldPostgresqlDb.ID > 0 { - if oldPostgresqlDb.Username != dbConfig.DbUser || oldPostgresqlDb.Password != dbConfig.Password { - return buserr.New(constant.ErrDbUserNotValid) - } - } else { - var createPostgresql dto.PostgresqlDBCreate - createPostgresql.Name = dbConfig.DbName - createPostgresql.Username = dbConfig.DbUser - createPostgresql.Database = database.Name - createPostgresql.Format = "UTF8" - createPostgresql.Password = dbConfig.Password - createPostgresql.From = database.From - createPostgresql.SuperUser = true - pgdb, err := NewIPostgresqlService().Create(ctx, createPostgresql) - if err != nil { - return err - } - resourceId = pgdb.ID - } - case constant.AppMysql, constant.AppMariaDB: - iMysqlRepo := repo.NewIMysqlRepo() - oldMysqlDb, _ := iMysqlRepo.Get(commonRepo.WithByName(dbConfig.DbName), iMysqlRepo.WithByFrom(constant.ResourceLocal)) - resourceId = oldMysqlDb.ID - if oldMysqlDb.ID > 0 { - if oldMysqlDb.Username != dbConfig.DbUser || oldMysqlDb.Password != dbConfig.Password { - return buserr.New(constant.ErrDbUserNotValid) - } - } else { - var createMysql dto.MysqlDBCreate - createMysql.Name = dbConfig.DbName - createMysql.Username = dbConfig.DbUser - createMysql.Database = database.Name - createMysql.Format = "utf8mb4" - createMysql.Permission = "%" - createMysql.Password = dbConfig.Password - createMysql.From = database.From - mysqldb, err := NewIMysqlService().Create(ctx, createMysql) - if err != nil { - return err - } - resourceId = mysqldb.ID - } - } - - } - var installResource model.AppInstallResource - installResource.ResourceId = resourceId - installResource.AppInstallId = appInstall.ID - if database.AppInstallID > 0 { - installResource.LinkId = database.AppInstallID - } else { - installResource.LinkId = database.ID - } - installResource.Key = database.Type - installResource.From = database.From - if err := appInstallResourceRepo.Create(ctx, &installResource); err != nil { - return err - } - } - return nil -} - -func handleAppInstallErr(ctx context.Context, install *model.AppInstall) error { - op := files.NewFileOp() - appDir := install.GetPath() - dir, _ := os.Stat(appDir) - if dir != nil { - _, _ = compose.Down(install.GetComposePath()) - if err := op.DeleteDir(appDir); err != nil { - return err - } - } - if err := deleteLink(ctx, install, true, true, true); err != nil { - return err - } - return nil -} - -func deleteAppInstall(install model.AppInstall, deleteBackup bool, forceDelete bool, deleteDB bool) error { - op := files.NewFileOp() - appDir := install.GetPath() - dir, _ := os.Stat(appDir) - if dir != nil { - out, err := compose.Down(install.GetComposePath()) - if err != nil && !forceDelete { - return handleErr(install, err, out) - } - if err = runScript(&install, "uninstall"); err != nil { - _, _ = compose.Up(install.GetComposePath()) - return err - } - } - tx, ctx := helper.GetTxAndContext() - defer tx.Rollback() - if err := appInstallRepo.Delete(ctx, install); err != nil { - return err - } - if err := deleteLink(ctx, &install, deleteDB, forceDelete, deleteBackup); err != nil && !forceDelete { - return err - } - - if DatabaseKeys[install.App.Key] > 0 { - _ = databaseRepo.Delete(ctx, databaseRepo.WithAppInstallID(install.ID)) - } - - switch install.App.Key { - case constant.AppOpenresty: - websites, _ := websiteRepo.List() - for _, website := range websites { - if website.AppInstallID > 0 { - websiteAppInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) - if websiteAppInstall.AppId > 0 { - websiteApp, _ := appRepo.GetFirst(commonRepo.WithByID(websiteAppInstall.AppId)) - if websiteApp.Type == constant.RuntimePHP { - go func() { - _, _ = compose.Down(websiteAppInstall.GetComposePath()) - _ = op.DeleteDir(websiteAppInstall.GetPath()) - }() - _ = appInstallRepo.Delete(ctx, websiteAppInstall) - } - } - } - } - _ = websiteRepo.DeleteAll(ctx) - _ = websiteDomainRepo.DeleteAll(ctx) - xpack.RemoveTamper("") - case constant.AppMysql, constant.AppMariaDB: - _ = mysqlRepo.Delete(ctx, mysqlRepo.WithByMysqlName(install.Name)) - case constant.AppPostgresql: - _ = postgresqlRepo.Delete(ctx, postgresqlRepo.WithByPostgresqlName(install.Name)) - } - - _ = backupRepo.DeleteRecord(ctx, commonRepo.WithByType("app"), commonRepo.WithByName(install.App.Key), backupRepo.WithByDetailName(install.Name)) - uploadDir := path.Join(global.CONF.System.BaseDir, fmt.Sprintf("1panel/uploads/app/%s/%s", install.App.Key, install.Name)) - if _, err := os.Stat(uploadDir); err == nil { - _ = os.RemoveAll(uploadDir) - } - if deleteBackup { - localDir, _ := loadLocalDir() - backupDir := path.Join(localDir, fmt.Sprintf("app/%s/%s", install.App.Key, install.Name)) - if _, err := os.Stat(backupDir); err == nil { - _ = os.RemoveAll(backupDir) - } - global.LOG.Infof("delete app %s-%s backups successful", install.App.Key, install.Name) - } - _ = op.DeleteDir(appDir) - tx.Commit() - return nil -} - -func deleteLink(ctx context.Context, install *model.AppInstall, deleteDB bool, forceDelete bool, deleteBackup bool) error { - resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID)) - if len(resources) == 0 { - return nil - } - for _, re := range resources { - if deleteDB { - switch re.Key { - case constant.AppMysql, constant.AppMariaDB: - mysqlService := NewIMysqlService() - database, _ := mysqlRepo.Get(commonRepo.WithByID(re.ResourceId)) - if reflect.DeepEqual(database, model.DatabaseMysql{}) { - continue - } - if err := mysqlService.Delete(ctx, dto.MysqlDBDelete{ - ID: database.ID, - ForceDelete: forceDelete, - DeleteBackup: deleteBackup, - Type: re.Key, - Database: database.MysqlName, - }); err != nil && !forceDelete { - return err - } - case constant.AppPostgresql: - pgsqlService := NewIPostgresqlService() - database, _ := postgresqlRepo.Get(commonRepo.WithByID(re.ResourceId)) - if reflect.DeepEqual(database, model.DatabasePostgresql{}) { - continue - } - if err := pgsqlService.Delete(ctx, dto.PostgresqlDBDelete{ - ID: database.ID, - ForceDelete: forceDelete, - DeleteBackup: deleteBackup, - Type: re.Key, - Database: database.PostgresqlName, - }); err != nil { - return err - } - } - } - - } - return appInstallResourceRepo.DeleteBy(ctx, appInstallResourceRepo.WithAppInstallId(install.ID)) -} - -func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string, error) { - if detail.DockerCompose == "" { - return "", nil - } - composeMap := make(map[string]interface{}) - if err := yaml.Unmarshal([]byte(detail.DockerCompose), &composeMap); err != nil { - return "", err - } - value, ok := composeMap["services"] - if !ok || value == nil { - return "", buserr.New(constant.ErrFileParse) - } - servicesMap := value.(map[string]interface{}) - if len(servicesMap) == 1 { - index := 0 - oldServiceName := "" - for k := range servicesMap { - oldServiceName = k - index++ - if index > 0 { - break - } - } - servicesMap[install.ServiceName] = servicesMap[oldServiceName] - if install.ServiceName != oldServiceName { - delete(servicesMap, oldServiceName) - } - } - envs := make(map[string]interface{}) - if err := json.Unmarshal([]byte(install.Env), &envs); err != nil { - return "", err - } - config := getAppCommonConfig(envs) - if config.ContainerName == "" { - config.ContainerName = install.ContainerName - envs[constant.ContainerName] = install.ContainerName - } - config.Advanced = true - if err := addDockerComposeCommonParam(composeMap, install.ServiceName, config, envs); err != nil { - return "", err - } - paramByte, err := json.Marshal(envs) - if err != nil { - return "", err - } - install.Env = string(paramByte) - composeByte, err := yaml.Marshal(composeMap) - if err != nil { - return "", err - } - return string(composeByte), nil -} - -func upgradeInstall(req request.AppInstallUpgrade) error { - install, err := appInstallRepo.GetFirst(commonRepo.WithByID(req.InstallID)) - if err != nil { - return err - } - detail, err := appDetailRepo.GetFirst(commonRepo.WithByID(req.DetailID)) - if err != nil { - return err - } - if install.Version == detail.Version { - return errors.New("two version is same") - } - install.Status = constant.Upgrading - - go func() { - var ( - upErr error - backupFile string - preErr error - ) - global.LOG.Infof(i18n.GetMsgWithName("UpgradeAppStart", install.Name, nil)) - if req.Backup { - backupRecord, err := NewIBackupService().AppBackup(dto.CommonBackup{Name: install.App.Key, DetailName: install.Name}) - if err == nil { - localDir, err := loadLocalDir() - if err == nil { - backupFile = path.Join(localDir, backupRecord.FileDir, backupRecord.FileName) - } else { - global.LOG.Errorf(i18n.GetMsgWithName("ErrAppBackup", install.Name, err)) - } - } else { - global.LOG.Errorf(i18n.GetMsgWithName("ErrAppBackup", install.Name, err)) - } - } - - defer func() { - if upErr != nil { - global.LOG.Infof(i18n.GetMsgWithName("ErrAppUpgrade", install.Name, upErr)) - if req.Backup { - global.LOG.Infof(i18n.GetMsgWithName("AppRecover", install.Name, nil)) - if err := NewIBackupService().AppRecover(dto.CommonRecover{Name: install.App.Key, DetailName: install.Name, Type: "app", Source: constant.ResourceLocal, File: backupFile}); err != nil { - global.LOG.Errorf("recover app [%s] [%s] failed %v", install.App.Key, install.Name, err) - } - } - existInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(req.InstallID)) - if existInstall.ID > 0 { - existInstall.Status = constant.UpgradeErr - existInstall.Message = upErr.Error() - _ = appInstallRepo.Save(context.Background(), &existInstall) - } - } - if preErr != nil { - global.LOG.Infof(i18n.GetMsgWithName("ErrAppUpgrade", install.Name, preErr)) - existInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(req.InstallID)) - if existInstall.ID > 0 { - existInstall.Status = constant.UpgradeErr - existInstall.Message = preErr.Error() - _ = appInstallRepo.Save(context.Background(), &existInstall) - } - } - }() - - fileOp := files.NewFileOp() - detailDir := path.Join(constant.ResourceDir, "apps", install.App.Resource, install.App.Key, detail.Version) - if install.App.Resource == constant.AppResourceRemote { - if preErr = downloadApp(install.App, detail, &install); preErr != nil { - return - } - if detail.DockerCompose == "" { - composeDetail, err := fileOp.GetContent(path.Join(detailDir, "docker-compose.yml")) - if err != nil { - preErr = err - return - } - detail.DockerCompose = string(composeDetail) - _ = appDetailRepo.Update(context.Background(), detail) - } - go func() { - _, _, _ = httpUtil.HandleGet(detail.DownloadCallBackUrl, http.MethodGet, constant.TimeOut5s) - }() - } - - if install.App.Resource == constant.AppResourceLocal { - detailDir = path.Join(constant.ResourceDir, "apps", "local", strings.TrimPrefix(install.App.Key, "local"), detail.Version) - } - - content, err := fileOp.GetContent(install.GetEnvPath()) - if err != nil { - preErr = err - return - } - if req.PullImage { - projectName := strings.ToLower(install.Name) - images, err := composeV2.GetDockerComposeImages(projectName, content, []byte(detail.DockerCompose)) - if err != nil { - preErr = err - return - } - for _, image := range images { - global.LOG.Infof(i18n.GetMsgWithName("PullImageStart", image, nil)) - if out, err := cmd.ExecWithTimeOut("docker pull "+image, 20*time.Minute); err != nil { - if out != "" { - err = errors.New(out) - } - preErr = buserr.WithNameAndErr("ErrDockerPullImage", "", err) - return - } else { - global.LOG.Infof(i18n.GetMsgByKey("PullImageSuccess")) - } - } - } - - command := exec.Command("/bin/bash", "-c", fmt.Sprintf("cp -rn %s/* %s || true", detailDir, install.GetPath())) - stdout, _ := command.CombinedOutput() - if stdout != nil { - global.LOG.Infof("upgrade app [%s] [%s] cp file log : %s ", install.App.Key, install.Name, string(stdout)) - } - sourceScripts := path.Join(detailDir, "scripts") - if fileOp.Stat(sourceScripts) { - dstScripts := path.Join(install.GetPath(), "scripts") - _ = fileOp.DeleteDir(dstScripts) - _ = fileOp.CreateDir(dstScripts, 0755) - scriptCmd := exec.Command("cp", "-rf", sourceScripts+"/.", dstScripts+"/") - _, _ = scriptCmd.CombinedOutput() - } - - var newCompose string - if req.DockerCompose == "" { - newCompose, upErr = getUpgradeCompose(install, detail) - if upErr != nil { - return - } - } else { - newCompose = req.DockerCompose - } - - install.DockerCompose = newCompose - install.Version = detail.Version - install.AppDetailId = req.DetailID - - if out, err := compose.Down(install.GetComposePath()); err != nil { - if out != "" { - upErr = errors.New(out) - return - } - upErr = err - return - } - envs := make(map[string]interface{}) - if upErr = json.Unmarshal([]byte(install.Env), &envs); upErr != nil { - return - } - envParams := make(map[string]string, len(envs)) - handleMap(envs, envParams) - if upErr = env.Write(envParams, install.GetEnvPath()); upErr != nil { - return - } - - if upErr = runScript(&install, "upgrade"); upErr != nil { - return - } - - if upErr = fileOp.WriteFile(install.GetComposePath(), strings.NewReader(install.DockerCompose), 0775); upErr != nil { - return - } - if out, err := compose.Up(install.GetComposePath()); err != nil { - if out != "" { - upErr = errors.New(out) - return - } - upErr = err - return - } - install.Status = constant.Running - _ = appInstallRepo.Save(context.Background(), &install) - global.LOG.Infof(i18n.GetMsgWithName("UpgradeAppSuccess", install.Name, nil)) - }() - - return appInstallRepo.Save(context.Background(), &install) -} - -func getContainerNames(install model.AppInstall) ([]string, error) { - envStr, err := coverEnvJsonToStr(install.Env) - if err != nil { - return nil, err - } - project, err := composeV2.GetComposeProject(install.Name, install.GetPath(), []byte(install.DockerCompose), []byte(envStr), true) - if err != nil { - return nil, err - } - containerMap := make(map[string]struct{}) - for _, service := range project.AllServices() { - if service.ContainerName == "${CONTAINER_NAME}" || service.ContainerName == "" { - continue - } - containerMap[service.ContainerName] = struct{}{} - } - var containerNames []string - for k := range containerMap { - containerNames = append(containerNames, k) - } - if len(containerNames) == 0 { - containerNames = append(containerNames, install.ContainerName) - } - return containerNames, nil -} - -func coverEnvJsonToStr(envJson string) (string, error) { - envMap := make(map[string]interface{}) - _ = json.Unmarshal([]byte(envJson), &envMap) - newEnvMap := make(map[string]string, len(envMap)) - handleMap(envMap, newEnvMap) - envStr, err := gotenv.Marshal(newEnvMap) - if err != nil { - return "", err - } - return envStr, nil -} - -func checkLimit(app model.App) error { - if app.Limit > 0 { - installs, err := appInstallRepo.ListBy(appInstallRepo.WithAppId(app.ID)) - if err != nil { - return err - } - if len(installs) >= app.Limit { - return buserr.New(constant.ErrAppLimit) - } - } - return nil -} - -func checkRequiredAndLimit(app model.App) error { - if err := checkLimit(app); err != nil { - return err - } - return nil -} - -func handleMap(params map[string]interface{}, envParams map[string]string) { - for k, v := range params { - switch t := v.(type) { - case string: - envParams[k] = t - case float64: - envParams[k] = strconv.FormatFloat(t, 'f', -1, 32) - case uint: - envParams[k] = strconv.Itoa(int(t)) - case int: - envParams[k] = strconv.Itoa(t) - case []interface{}: - strArray := make([]string, len(t)) - for i := range t { - strArray[i] = strings.ToLower(fmt.Sprintf("%v", t[i])) - } - envParams[k] = strings.Join(strArray, ",") - case map[string]interface{}: - handleMap(t, envParams) - } - } -} - -func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.AppInstall) (err error) { - if app.IsLocalApp() { - //本地应用,不去官网下载 - return nil - } - appResourceDir := path.Join(constant.AppResourceDir, app.Resource) - appDownloadDir := app.GetAppResourcePath() - appVersionDir := path.Join(appDownloadDir, appDetail.Version) - fileOp := files.NewFileOp() - if !appDetail.Update && fileOp.Stat(appVersionDir) { - return - } - if !fileOp.Stat(appDownloadDir) { - _ = fileOp.CreateDir(appDownloadDir, 0755) - } - if !fileOp.Stat(appVersionDir) { - _ = fileOp.CreateDir(appVersionDir, 0755) - } - global.LOG.Infof("download app[%s] from %s", app.Name, appDetail.DownloadUrl) - filePath := path.Join(appVersionDir, app.Key+"-"+appDetail.Version+".tar.gz") - - defer func() { - if err != nil { - if appInstall != nil { - appInstall.Status = constant.DownloadErr - appInstall.Message = err.Error() - } - } - }() - - if err = fileOp.DownloadFileWithProxy(appDetail.DownloadUrl, filePath); err != nil { - global.LOG.Errorf("download app[%s] error %v", app.Name, err) - return - } - if err = fileOp.Decompress(filePath, appResourceDir, files.SdkTarGz, ""); err != nil { - global.LOG.Errorf("decompress app[%s] error %v", app.Name, err) - return - } - _ = fileOp.DeleteFile(filePath) - appDetail.Update = false - _ = appDetailRepo.Update(context.Background(), appDetail) - return -} - -func copyData(app model.App, appDetail model.AppDetail, appInstall *model.AppInstall, req request.AppInstallCreate) (err error) { - fileOp := files.NewFileOp() - appResourceDir := path.Join(constant.AppResourceDir, app.Resource) - - if app.Resource == constant.AppResourceRemote { - err = downloadApp(app, appDetail, appInstall) - if err != nil { - return - } - go func() { - _, _, _ = httpUtil.HandleGet(appDetail.DownloadCallBackUrl, http.MethodGet, constant.TimeOut5s) - }() - } - appKey := app.Key - installAppDir := path.Join(constant.AppInstallDir, app.Key) - if app.Resource == constant.AppResourceLocal { - appResourceDir = constant.LocalAppResourceDir - appKey = strings.TrimPrefix(app.Key, "local") - installAppDir = path.Join(constant.LocalAppInstallDir, appKey) - } - resourceDir := path.Join(appResourceDir, appKey, appDetail.Version) - - if !fileOp.Stat(installAppDir) { - if err = fileOp.CreateDir(installAppDir, 0755); err != nil { - return - } - } - appDir := path.Join(installAppDir, req.Name) - if fileOp.Stat(appDir) { - if err = fileOp.DeleteDir(appDir); err != nil { - return - } - } - if err = fileOp.Copy(resourceDir, installAppDir); err != nil { - return - } - versionDir := path.Join(installAppDir, appDetail.Version) - if err = fileOp.Rename(versionDir, appDir); err != nil { - return - } - envPath := path.Join(appDir, ".env") - - envParams := make(map[string]string, len(req.Params)) - handleMap(req.Params, envParams) - if err = env.Write(envParams, envPath); err != nil { - return - } - if err := fileOp.WriteFile(appInstall.GetComposePath(), strings.NewReader(appInstall.DockerCompose), 0755); err != nil { - return err - } - return -} - -func runScript(appInstall *model.AppInstall, operate string) error { - workDir := appInstall.GetPath() - scriptPath := "" - switch operate { - case "init": - scriptPath = path.Join(workDir, "scripts", "init.sh") - case "upgrade": - scriptPath = path.Join(workDir, "scripts", "upgrade.sh") - case "uninstall": - scriptPath = path.Join(workDir, "scripts", "uninstall.sh") - } - if !files.NewFileOp().Stat(scriptPath) { - return nil - } - out, err := cmd.ExecScript(scriptPath, workDir) - if err != nil { - if out != "" { - errMsg := fmt.Sprintf("run script %s error %s", scriptPath, out) - global.LOG.Error(errMsg) - return errors.New(errMsg) - } - return err - } - return nil -} - -func checkContainerNameIsExist(containerName, appDir string) (bool, error) { - client, err := composeV2.NewDockerClient() - if err != nil { - return false, err - } - defer client.Close() - var options container.ListOptions - list, err := client.ContainerList(context.Background(), options) - if err != nil { - return false, err - } - for _, container := range list { - if containerName == container.Names[0][1:] { - if workDir, ok := container.Labels[composeWorkdirLabel]; ok { - if workDir != appDir { - return true, nil - } - } else { - return true, nil - } - } - - } - return false, nil -} - -func upApp(appInstall *model.AppInstall, pullImages bool) { - upProject := func(appInstall *model.AppInstall) (err error) { - var ( - out string - errMsg string - ) - if pullImages && appInstall.App.Type != "php" { - out, err = compose.Pull(appInstall.GetComposePath()) - if err != nil { - if out != "" { - if strings.Contains(out, "no such host") { - errMsg = i18n.GetMsgByKey("ErrNoSuchHost") + ":" - } - if strings.Contains(out, "timeout") { - errMsg = i18n.GetMsgByKey("ErrImagePullTimeOut") + ":" - } - appInstall.Message = errMsg + out - } - return err - } - } - - out, err = compose.Up(appInstall.GetComposePath()) - if err != nil { - if out != "" { - appInstall.Message = errMsg + out - } - return err - } - return - } - if err := upProject(appInstall); err != nil { - appInstall.Status = constant.UpErr - } else { - appInstall.Status = constant.Running - } - exist, _ := appInstallRepo.GetFirst(commonRepo.WithByID(appInstall.ID)) - if exist.ID > 0 { - containerNames, err := getContainerNames(*appInstall) - if err != nil { - return - } - if len(containerNames) > 0 { - appInstall.ContainerName = strings.Join(containerNames, ",") - } - _ = appInstallRepo.Save(context.Background(), appInstall) - } -} - -func rebuildApp(appInstall model.AppInstall) error { - appInstall.Status = constant.Rebuilding - _ = appInstallRepo.Save(context.Background(), &appInstall) - go func() { - dockerComposePath := appInstall.GetComposePath() - out, err := compose.Down(dockerComposePath) - if err != nil { - _ = handleErr(appInstall, err, out) - return - } - out, err = compose.Up(appInstall.GetComposePath()) - if err != nil { - _ = handleErr(appInstall, err, out) - return - } - containerNames, err := getContainerNames(appInstall) - if err != nil { - _ = handleErr(appInstall, err, out) - return - } - appInstall.ContainerName = strings.Join(containerNames, ",") - - appInstall.Status = constant.Running - _ = appInstallRepo.Save(context.Background(), &appInstall) - }() - return nil -} - -func getAppDetails(details []model.AppDetail, versions []dto.AppConfigVersion) map[string]model.AppDetail { - appDetails := make(map[string]model.AppDetail, len(details)) - for _, old := range details { - old.Status = constant.AppTakeDown - appDetails[old.Version] = old - } - for _, v := range versions { - version := v.Name - detail, ok := appDetails[version] - if ok { - detail.Status = constant.AppNormal - appDetails[version] = detail - } else { - appDetails[version] = model.AppDetail{ - Version: version, - Status: constant.AppNormal, - } - } - } - return appDetails -} - -func getApps(oldApps []model.App, items []dto.AppDefine) map[string]model.App { - apps := make(map[string]model.App, len(oldApps)) - for _, old := range oldApps { - old.Status = constant.AppTakeDown - apps[old.Key] = old - } - for _, item := range items { - config := item.AppProperty - key := config.Key - app, ok := apps[key] - if !ok { - app = model.App{} - } - app.Resource = constant.AppResourceRemote - app.Name = item.Name - app.Limit = config.Limit - app.Key = key - app.ShortDescZh = config.ShortDescZh - app.ShortDescEn = config.ShortDescEn - app.Website = config.Website - app.Document = config.Document - app.Github = config.Github - app.Type = config.Type - app.CrossVersionUpdate = config.CrossVersionUpdate - app.Status = constant.AppNormal - app.LastModified = item.LastModified - app.ReadMe = item.ReadMe - apps[key] = app - } - return apps -} - -func handleLocalAppDetail(versionDir string, appDetail *model.AppDetail) error { - fileOp := files.NewFileOp() - dockerComposePath := path.Join(versionDir, "docker-compose.yml") - if !fileOp.Stat(dockerComposePath) { - return buserr.WithName(constant.ErrFileNotFound, "docker-compose.yml") - } - dockerComposeByte, _ := fileOp.GetContent(dockerComposePath) - if dockerComposeByte == nil { - return buserr.WithName(constant.ErrFileParseApp, "docker-compose.yml") - } - appDetail.DockerCompose = string(dockerComposeByte) - paramPath := path.Join(versionDir, "data.yml") - if !fileOp.Stat(paramPath) { - return buserr.WithName(constant.ErrFileNotFound, "data.yml") - } - paramByte, _ := fileOp.GetContent(paramPath) - if paramByte == nil { - return buserr.WithName(constant.ErrFileNotFound, "data.yml") - } - appParamConfig := dto.LocalAppParam{} - if err := yaml.Unmarshal(paramByte, &appParamConfig); err != nil { - return buserr.WithMap(constant.ErrFileParseApp, map[string]interface{}{"name": "data.yml", "err": err.Error()}, err) - } - dataJson, err := json.Marshal(appParamConfig.AppParams) - if err != nil { - return buserr.WithMap(constant.ErrFileParseApp, map[string]interface{}{"name": "data.yml", "err": err.Error()}, err) - } - var appParam dto.AppForm - if err = json.Unmarshal(dataJson, &appParam); err != nil { - return buserr.WithMap(constant.ErrFileParseApp, map[string]interface{}{"name": "data.yml", "err": err.Error()}, err) - } - for _, formField := range appParam.FormFields { - if strings.Contains(formField.EnvKey, " ") { - return buserr.WithName(constant.ErrAppParamKey, formField.EnvKey) - } - } - - var dataMap map[string]interface{} - err = yaml.Unmarshal(paramByte, &dataMap) - if err != nil { - return buserr.WithMap(constant.ErrFileParseApp, map[string]interface{}{"name": "data.yml", "err": err.Error()}, err) - } - - additionalProperties, ok := dataMap["additionalProperties"].(map[string]interface{}) - if !ok { - return buserr.WithName(constant.ErrAppParamKey, "additionalProperties") - } - - formFieldsInterface, ok := additionalProperties["formFields"] - if ok { - formFields, ok := formFieldsInterface.([]interface{}) - if !ok { - return buserr.WithName(constant.ErrAppParamKey, "formFields") - } - for _, item := range formFields { - field := item.(map[string]interface{}) - for key, value := range field { - if value == nil { - return buserr.WithName(constant.ErrAppParamKey, key) - } - } - } - } - - appDetail.Params = string(dataJson) - return nil -} - -func handleLocalApp(appDir string) (app *model.App, err error) { - fileOp := files.NewFileOp() - configYamlPath := path.Join(appDir, "data.yml") - if !fileOp.Stat(configYamlPath) { - err = buserr.WithName(constant.ErrFileNotFound, "data.yml") - return - } - iconPath := path.Join(appDir, "logo.png") - if !fileOp.Stat(iconPath) { - err = buserr.WithName(constant.ErrFileNotFound, "logo.png") - return - } - configYamlByte, err := fileOp.GetContent(configYamlPath) - if err != nil { - err = buserr.WithMap(constant.ErrFileParseApp, map[string]interface{}{"name": "data.yml", "err": err.Error()}, err) - return - } - localAppDefine := dto.LocalAppAppDefine{} - if err = yaml.Unmarshal(configYamlByte, &localAppDefine); err != nil { - err = buserr.WithMap(constant.ErrFileParseApp, map[string]interface{}{"name": "data.yml", "err": err.Error()}, err) - return - } - app = &localAppDefine.AppProperty - app.Resource = constant.AppResourceLocal - app.Status = constant.AppNormal - app.Recommend = 9999 - app.TagsKey = append(app.TagsKey, "Local") - app.Key = "local" + app.Key - readMePath := path.Join(appDir, "README.md") - readMeByte, err := fileOp.GetContent(readMePath) - if err == nil { - app.ReadMe = string(readMeByte) - } - iconByte, _ := fileOp.GetContent(iconPath) - if iconByte != nil { - iconStr := base64.StdEncoding.EncodeToString(iconByte) - app.Icon = iconStr - } - return -} - -func handleErr(install model.AppInstall, err error, out string) error { - reErr := err - install.Message = err.Error() - if out != "" { - install.Message = out - reErr = errors.New(out) - } - install.Status = constant.UpErr - _ = appInstallRepo.Save(context.Background(), &install) - return reErr -} - -func doNotNeedSync(installed model.AppInstall) bool { - return installed.Status == constant.Installing || installed.Status == constant.Rebuilding || installed.Status == constant.Upgrading || - installed.Status == constant.Syncing -} - -func synAppInstall(containers map[string]types.Container, appInstall *model.AppInstall, force bool) { - containerNames := strings.Split(appInstall.ContainerName, ",") - if len(containers) == 0 { - if appInstall.Status == constant.UpErr && !force { - return - } - appInstall.Status = constant.Error - appInstall.Message = buserr.WithName("ErrContainerNotFound", strings.Join(containerNames, ",")).Error() - _ = appInstallRepo.Save(context.Background(), appInstall) - return - } - notFoundNames := make([]string, 0) - exitNames := make([]string, 0) - exitedCount := 0 - pausedCount := 0 - runningCount := 0 - total := len(containerNames) - for _, name := range containerNames { - if con, ok := containers["/"+name]; ok { - switch con.State { - case "exited": - exitedCount++ - exitNames = append(exitNames, name) - case "running": - runningCount++ - case "paused": - pausedCount++ - } - } else { - notFoundNames = append(notFoundNames, name) - } - } - switch { - case exitedCount == total: - appInstall.Status = constant.Stopped - case runningCount == total: - appInstall.Status = constant.Running - case pausedCount == total: - appInstall.Status = constant.Paused - case len(notFoundNames) == total: - if appInstall.Status == constant.UpErr && !force { - return - } - appInstall.Status = constant.Error - appInstall.Message = buserr.WithName("ErrContainerNotFound", strings.Join(notFoundNames, ",")).Error() - default: - var msg string - if exitedCount > 0 { - msg = buserr.WithName("ErrContainerMsg", strings.Join(exitNames, ",")).Error() - } - if len(notFoundNames) > 0 { - msg += buserr.WithName("ErrContainerNotFound", strings.Join(notFoundNames, ",")).Error() - } - if msg == "" { - msg = buserr.New("ErrAppWarn").Error() - } - appInstall.Message = msg - appInstall.Status = constant.UnHealthy - } - _ = appInstallRepo.Save(context.Background(), appInstall) -} - -func handleInstalled(appInstallList []model.AppInstall, updated bool, sync bool) ([]response.AppInstallDTO, error) { - var ( - res []response.AppInstallDTO - containersMap map[string]types.Container - ) - if sync { - cli, err := docker.NewClient() - if err != nil { - return nil, err - } - defer cli.Close() - containers, err := cli.ListAllContainers() - if err != nil { - return nil, err - } - containersMap = make(map[string]types.Container, len(containers)) - for _, contain := range containers { - containersMap[contain.Names[0]] = contain - } - } - - for _, installed := range appInstallList { - if updated && (installed.App.Type == "php" || installed.Status == constant.Installing || (installed.App.Key == constant.AppMysql && installed.Version == "5.6.51")) { - continue - } - if sync && !doNotNeedSync(installed) { - synAppInstall(containersMap, &installed, false) - } - - installDTO := response.AppInstallDTO{ - ID: installed.ID, - Name: installed.Name, - AppID: installed.AppId, - AppDetailID: installed.AppDetailId, - Version: installed.Version, - Status: installed.Status, - Message: installed.Message, - HttpPort: installed.HttpPort, - HttpsPort: installed.HttpsPort, - Icon: installed.App.Icon, - AppName: installed.App.Name, - AppKey: installed.App.Key, - AppType: installed.App.Type, - Path: installed.GetPath(), - CreatedAt: installed.CreatedAt, - App: response.AppDetail{ - Github: installed.App.Github, - Website: installed.App.Website, - Document: installed.App.Document, - }, - } - if updated { - installDTO.DockerCompose = installed.DockerCompose - } - app, err := appRepo.GetFirst(commonRepo.WithByID(installed.AppId)) - if err != nil { - return nil, err - } - details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(app.ID)) - if err != nil { - return nil, err - } - var versions []string - for _, detail := range details { - if detail.IgnoreUpgrade || installed.Version == "latest" { - continue - } - if common.IsCrossVersion(installed.Version, detail.Version) && !app.CrossVersionUpdate { - continue - } - versions = append(versions, detail.Version) - } - versions = common.GetSortedVersions(versions) - if len(versions) == 0 { - if !updated { - installDTO.CanUpdate = false - res = append(res, installDTO) - } - continue - } - lastVersion := versions[0] - if common.IsCrossVersion(installed.Version, lastVersion) { - installDTO.CanUpdate = app.CrossVersionUpdate - } else { - installDTO.CanUpdate = common.CompareVersion(lastVersion, installed.Version) - } - if updated { - if installDTO.CanUpdate { - res = append(res, installDTO) - } - } else { - res = append(res, installDTO) - } - } - return res, nil -} - -func getAppInstallByKey(key string) (model.AppInstall, error) { - app, err := appRepo.GetFirst(appRepo.WithKey(key)) - if err != nil { - return model.AppInstall{}, err - } - appInstall, err := appInstallRepo.GetFirst(appInstallRepo.WithAppId(app.ID)) - if err != nil { - return model.AppInstall{}, err - } - return appInstall, nil -} - -func getAppInstallPort(key string) (httpPort, httpsPort int, err error) { - install, err := getAppInstallByKey(key) - if err != nil { - return - } - httpPort = install.HttpPort - httpsPort = install.HttpsPort - return -} - -func updateToolApp(installed *model.AppInstall) { - tooKey, ok := dto.AppToolMap[installed.App.Key] - if !ok { - return - } - toolInstall, _ := getAppInstallByKey(tooKey) - if reflect.DeepEqual(toolInstall, model.AppInstall{}) { - return - } - paramMap := make(map[string]string) - _ = json.Unmarshal([]byte(installed.Param), ¶mMap) - envMap := make(map[string]interface{}) - _ = json.Unmarshal([]byte(toolInstall.Env), &envMap) - if password, ok := paramMap["PANEL_DB_ROOT_PASSWORD"]; ok { - envMap["PANEL_DB_ROOT_PASSWORD"] = password - } - if _, ok := envMap["PANEL_REDIS_HOST"]; ok { - envMap["PANEL_REDIS_HOST"] = installed.ServiceName - } - if _, ok := envMap["PANEL_DB_HOST"]; ok { - envMap["PANEL_DB_HOST"] = installed.ServiceName - } - - envPath := path.Join(toolInstall.GetPath(), ".env") - contentByte, err := json.Marshal(envMap) - if err != nil { - global.LOG.Errorf("update tool app [%s] error : %s", toolInstall.Name, err.Error()) - return - } - envFileMap := make(map[string]string) - handleMap(envMap, envFileMap) - if err = env.Write(envFileMap, envPath); err != nil { - global.LOG.Errorf("update tool app [%s] error : %s", toolInstall.Name, err.Error()) - return - } - toolInstall.Env = string(contentByte) - if err := appInstallRepo.Save(context.Background(), &toolInstall); err != nil { - global.LOG.Errorf("update tool app [%s] error : %s", toolInstall.Name, err.Error()) - return - } - if out, err := compose.Down(toolInstall.GetComposePath()); err != nil { - global.LOG.Errorf("update tool app [%s] error : %s", toolInstall.Name, out) - return - } - if out, err := compose.Up(toolInstall.GetComposePath()); err != nil { - global.LOG.Errorf("update tool app [%s] error : %s", toolInstall.Name, out) - return - } -} - -func addDockerComposeCommonParam(composeMap map[string]interface{}, serviceName string, req request.AppContainerConfig, params map[string]interface{}) error { - services, serviceValid := composeMap["services"].(map[string]interface{}) - if !serviceValid { - return buserr.New(constant.ErrFileParse) - } - service, serviceExist := services[serviceName] - if !serviceExist { - return buserr.New(constant.ErrFileParse) - } - serviceValue := service.(map[string]interface{}) - - deploy := map[string]interface{}{} - if de, ok := serviceValue["deploy"]; ok { - deploy = de.(map[string]interface{}) - } - resource := map[string]interface{}{} - if res, ok := deploy["resources"]; ok { - resource = res.(map[string]interface{}) - } - resource["limits"] = map[string]interface{}{ - "cpus": "${CPUS}", - "memory": "${MEMORY_LIMIT}", - } - deploy["resources"] = resource - serviceValue["deploy"] = deploy - - ports, ok := serviceValue["ports"].([]interface{}) - if ok { - for i, port := range ports { - portStr, portOK := port.(string) - if !portOK { - continue - } - portArray := strings.Split(portStr, ":") - if len(portArray) == 2 { - portArray = append([]string{"${HOST_IP}"}, portArray...) - } - ports[i] = strings.Join(portArray, ":") - } - serviceValue["ports"] = ports - } - - params[constant.CPUS] = "0" - params[constant.MemoryLimit] = "0" - if req.Advanced { - if req.CpuQuota > 0 { - params[constant.CPUS] = req.CpuQuota - } - if req.MemoryLimit > 0 { - params[constant.MemoryLimit] = strconv.FormatFloat(req.MemoryLimit, 'f', -1, 32) + req.MemoryUnit - } - } - _, portExist := serviceValue["ports"].([]interface{}) - if portExist { - allowHost := "127.0.0.1" - if req.Advanced && req.AllowPort { - allowHost = "" - } - params[constant.HostIP] = allowHost - } - services[serviceName] = serviceValue - return nil -} - -func getAppCommonConfig(envs map[string]interface{}) request.AppContainerConfig { - config := request.AppContainerConfig{} - - if hostIp, ok := envs[constant.HostIP]; ok { - config.AllowPort = hostIp.(string) != "127.0.0.1" - } else { - config.AllowPort = true - } - if cpuCore, ok := envs[constant.CPUS]; ok { - numStr, ok := cpuCore.(string) - if ok { - num, err := strconv.ParseFloat(numStr, 64) - if err == nil { - config.CpuQuota = num - } - } else { - num64, flOk := cpuCore.(float64) - if flOk { - config.CpuQuota = num64 - } - } - } else { - config.CpuQuota = 0 - } - if memLimit, ok := envs[constant.MemoryLimit]; ok { - re := regexp.MustCompile(`(\d+)([A-Za-z]+)`) - matches := re.FindStringSubmatch(memLimit.(string)) - if len(matches) == 3 { - num, err := strconv.ParseFloat(matches[1], 64) - if err == nil { - unit := matches[2] - config.MemoryLimit = num - config.MemoryUnit = unit - } - } - } else { - config.MemoryLimit = 0 - config.MemoryUnit = "M" - } - - if containerName, ok := envs[constant.ContainerName]; ok { - config.ContainerName = containerName.(string) - } - - return config -} - -func isHostModel(dockerCompose string) bool { - composeMap := make(map[string]interface{}) - _ = yaml.Unmarshal([]byte(dockerCompose), &composeMap) - services, serviceValid := composeMap["services"].(map[string]interface{}) - if !serviceValid { - return false - } - for _, service := range services { - serviceValue := service.(map[string]interface{}) - if value, ok := serviceValue["network_mode"]; ok && value == "host" { - return true - } - } - return false -} diff --git a/backend/app/service/backup.go b/backend/app/service/backup.go deleted file mode 100644 index f355430e1..000000000 --- a/backend/app/service/backup.go +++ /dev/null @@ -1,637 +0,0 @@ -package service - -import ( - "bufio" - "context" - "encoding/base64" - "encoding/json" - "fmt" - "os" - "path" - "sort" - "strings" - "sync" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cloud_storage" - "github.com/1Panel-dev/1Panel/backend/utils/cloud_storage/client" - fileUtils "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/jinzhu/copier" - "github.com/pkg/errors" -) - -type BackupService struct{} - -type IBackupService interface { - List() ([]dto.BackupInfo, error) - SearchRecordsWithPage(search dto.RecordSearch) (int64, []dto.BackupRecords, error) - SearchRecordsByCronjobWithPage(search dto.RecordSearchByCronjob) (int64, []dto.BackupRecords, error) - LoadOneDriveInfo() (dto.OneDriveInfo, error) - DownloadRecord(info dto.DownloadRecord) (string, error) - Create(backupDto dto.BackupOperate) error - GetBuckets(backupDto dto.ForBuckets) ([]interface{}, error) - Update(ireq dto.BackupOperate) error - Delete(id uint) error - DeleteRecordByName(backupType, name, detailName string, withDeleteFile bool) error - BatchDeleteRecord(ids []uint) error - NewClient(backup *model.BackupAccount) (cloud_storage.CloudStorageClient, error) - - ListFiles(req dto.BackupSearchFile) []string - - MysqlBackup(db dto.CommonBackup) error - PostgresqlBackup(db dto.CommonBackup) error - MysqlRecover(db dto.CommonRecover) error - PostgresqlRecover(db dto.CommonRecover) error - MysqlRecoverByUpload(req dto.CommonRecover) error - PostgresqlRecoverByUpload(req dto.CommonRecover) error - - RedisBackup(db dto.CommonBackup) error - RedisRecover(db dto.CommonRecover) error - - WebsiteBackup(db dto.CommonBackup) error - WebsiteRecover(req dto.CommonRecover) error - - AppBackup(db dto.CommonBackup) (*model.BackupRecord, error) - AppRecover(req dto.CommonRecover) error - - Run() -} - -func NewIBackupService() IBackupService { - return &BackupService{} -} - -func (u *BackupService) List() ([]dto.BackupInfo, error) { - ops, err := backupRepo.List(commonRepo.WithOrderBy("created_at desc")) - var dtobas []dto.BackupInfo - dtobas = append(dtobas, u.loadByType("LOCAL", ops)) - dtobas = append(dtobas, u.loadByType("OSS", ops)) - dtobas = append(dtobas, u.loadByType("S3", ops)) - dtobas = append(dtobas, u.loadByType("SFTP", ops)) - dtobas = append(dtobas, u.loadByType("MINIO", ops)) - dtobas = append(dtobas, u.loadByType("COS", ops)) - dtobas = append(dtobas, u.loadByType("KODO", ops)) - dtobas = append(dtobas, u.loadByType("OneDrive", ops)) - dtobas = append(dtobas, u.loadByType("WebDAV", ops)) - return dtobas, err -} - -func (u *BackupService) SearchRecordsWithPage(search dto.RecordSearch) (int64, []dto.BackupRecords, error) { - total, records, err := backupRepo.PageRecord( - search.Page, search.PageSize, - commonRepo.WithOrderBy("created_at desc"), - commonRepo.WithByName(search.Name), - commonRepo.WithByType(search.Type), - backupRepo.WithByDetailName(search.DetailName), - ) - if err != nil { - return 0, nil, err - } - - datas, err := u.loadRecordSize(records) - sort.Slice(datas, func(i, j int) bool { - return datas[i].CreatedAt.After(datas[j].CreatedAt) - }) - return total, datas, err -} - -func (u *BackupService) SearchRecordsByCronjobWithPage(search dto.RecordSearchByCronjob) (int64, []dto.BackupRecords, error) { - total, records, err := backupRepo.PageRecord( - search.Page, search.PageSize, - commonRepo.WithOrderBy("created_at desc"), - backupRepo.WithByCronID(search.CronjobID), - ) - if err != nil { - return 0, nil, err - } - - datas, err := u.loadRecordSize(records) - sort.Slice(datas, func(i, j int) bool { - return datas[i].CreatedAt.After(datas[j].CreatedAt) - }) - return total, datas, err -} - -type loadSizeHelper struct { - isOk bool - backupPath string - client cloud_storage.CloudStorageClient -} - -func (u *BackupService) LoadOneDriveInfo() (dto.OneDriveInfo, error) { - var data dto.OneDriveInfo - data.RedirectUri = constant.OneDriveRedirectURI - clientID, err := settingRepo.Get(settingRepo.WithByKey("OneDriveID")) - if err != nil { - return data, err - } - idItem, err := base64.StdEncoding.DecodeString(clientID.Value) - if err != nil { - return data, err - } - data.ClientID = string(idItem) - clientSecret, err := settingRepo.Get(settingRepo.WithByKey("OneDriveSc")) - if err != nil { - return data, err - } - secretItem, err := base64.StdEncoding.DecodeString(clientSecret.Value) - if err != nil { - return data, err - } - data.ClientSecret = string(secretItem) - - return data, err -} - -func (u *BackupService) DownloadRecord(info dto.DownloadRecord) (string, error) { - if info.Source == "LOCAL" { - localDir, err := loadLocalDir() - if err != nil { - return "", err - } - return path.Join(localDir, info.FileDir, info.FileName), nil - } - backup, _ := backupRepo.Get(commonRepo.WithByType(info.Source)) - if backup.ID == 0 { - return "", constant.ErrRecordNotFound - } - varMap := make(map[string]interface{}) - if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil { - return "", err - } - varMap["bucket"] = backup.Bucket - switch backup.Type { - case constant.Sftp, constant.WebDAV: - varMap["username"] = backup.AccessKey - varMap["password"] = backup.Credential - case constant.OSS, constant.S3, constant.MinIo, constant.Cos, constant.Kodo: - varMap["accessKey"] = backup.AccessKey - varMap["secretKey"] = backup.Credential - case constant.OneDrive: - varMap["accessToken"] = backup.Credential - } - backClient, err := cloud_storage.NewCloudStorageClient(backup.Type, varMap) - if err != nil { - return "", fmt.Errorf("new cloud storage client failed, err: %v", err) - } - targetPath := fmt.Sprintf("%s/download/%s/%s", constant.DataDir, info.FileDir, info.FileName) - if _, err := os.Stat(path.Dir(targetPath)); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(path.Dir(targetPath), os.ModePerm); err != nil { - global.LOG.Errorf("mkdir %s failed, err: %v", path.Dir(targetPath), err) - } - } - srcPath := fmt.Sprintf("%s/%s", info.FileDir, info.FileName) - if len(backup.BackupPath) != 0 { - srcPath = path.Join(strings.TrimPrefix(backup.BackupPath, "/"), srcPath) - } - if exist, _ := backClient.Exist(srcPath); exist { - isOK, err := backClient.Download(srcPath, targetPath) - if !isOK { - return "", fmt.Errorf("cloud storage download failed, err: %v", err) - } - } - return targetPath, nil -} - -func (u *BackupService) Create(req dto.BackupOperate) error { - backup, _ := backupRepo.Get(commonRepo.WithByType(req.Type)) - if backup.ID != 0 { - return constant.ErrRecordExist - } - if err := copier.Copy(&backup, &req); err != nil { - return errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - - if req.Type == constant.OneDrive { - if err := u.loadAccessToken(&backup); err != nil { - return err - } - } - if req.Type != "LOCAL" { - if _, err := u.checkBackupConn(&backup); err != nil { - return buserr.WithMap("ErrBackupCheck", map[string]interface{}{"err": err.Error()}, err) - } - } - if backup.Type == constant.OneDrive { - StartRefreshOneDriveToken() - } - if err := backupRepo.Create(&backup); err != nil { - return err - } - return nil -} - -func (u *BackupService) GetBuckets(backupDto dto.ForBuckets) ([]interface{}, error) { - varMap := make(map[string]interface{}) - if err := json.Unmarshal([]byte(backupDto.Vars), &varMap); err != nil { - return nil, err - } - switch backupDto.Type { - case constant.Sftp, constant.WebDAV: - varMap["username"] = backupDto.AccessKey - varMap["password"] = backupDto.Credential - case constant.OSS, constant.S3, constant.MinIo, constant.Cos, constant.Kodo: - varMap["accessKey"] = backupDto.AccessKey - varMap["secretKey"] = backupDto.Credential - } - client, err := cloud_storage.NewCloudStorageClient(backupDto.Type, varMap) - if err != nil { - return nil, err - } - return client.ListBuckets() -} - -func (u *BackupService) Delete(id uint) error { - backup, _ := backupRepo.Get(commonRepo.WithByID(id)) - if backup.ID == 0 { - return constant.ErrRecordNotFound - } - if backup.Type == constant.OneDrive { - global.Cron.Remove(global.OneDriveCronID) - } - cronjobs, _ := cronjobRepo.List(cronjobRepo.WithByDefaultDownload(backup.Type)) - if len(cronjobs) != 0 { - return buserr.New(constant.ErrBackupInUsed) - } - return backupRepo.Delete(commonRepo.WithByID(id)) -} - -func (u *BackupService) DeleteRecordByName(backupType, name, detailName string, withDeleteFile bool) error { - if !withDeleteFile { - return backupRepo.DeleteRecord(context.Background(), commonRepo.WithByType(backupType), commonRepo.WithByName(name), backupRepo.WithByDetailName(detailName)) - } - - records, err := backupRepo.ListRecord(commonRepo.WithByType(backupType), commonRepo.WithByName(name), backupRepo.WithByDetailName(detailName)) - if err != nil { - return err - } - - for _, record := range records { - backupAccount, err := backupRepo.Get(commonRepo.WithByType(record.Source)) - if err != nil { - global.LOG.Errorf("load backup account %s info from db failed, err: %v", record.Source, err) - continue - } - client, err := u.NewClient(&backupAccount) - if err != nil { - global.LOG.Errorf("new client for backup account %s failed, err: %v", record.Source, err) - continue - } - if _, err = client.Delete(path.Join(record.FileDir, record.FileName)); err != nil { - global.LOG.Errorf("remove file %s from %s failed, err: %v", path.Join(record.FileDir, record.FileName), record.Source, err) - } - _ = backupRepo.DeleteRecord(context.Background(), commonRepo.WithByID(record.ID)) - } - return nil -} - -func (u *BackupService) BatchDeleteRecord(ids []uint) error { - records, err := backupRepo.ListRecord(commonRepo.WithIdsIn(ids)) - if err != nil { - return err - } - for _, record := range records { - backupAccount, err := backupRepo.Get(commonRepo.WithByType(record.Source)) - if err != nil { - global.LOG.Errorf("load backup account %s info from db failed, err: %v", record.Source, err) - continue - } - client, err := u.NewClient(&backupAccount) - if err != nil { - global.LOG.Errorf("new client for backup account %s failed, err: %v", record.Source, err) - continue - } - if _, err = client.Delete(path.Join(record.FileDir, record.FileName)); err != nil { - global.LOG.Errorf("remove file %s from %s failed, err: %v", path.Join(record.FileDir, record.FileName), record.Source, err) - } - } - return backupRepo.DeleteRecord(context.Background(), commonRepo.WithIdsIn(ids)) -} - -func (u *BackupService) Update(req dto.BackupOperate) error { - backup, err := backupRepo.Get(commonRepo.WithByID(req.ID)) - if err != nil { - return constant.ErrRecordNotFound - } - varMap := make(map[string]interface{}) - if err := json.Unmarshal([]byte(req.Vars), &varMap); err != nil { - return err - } - - oldVars := backup.Vars - oldDir, err := loadLocalDir() - if err != nil { - return err - } - upMap := make(map[string]interface{}) - upMap["bucket"] = req.Bucket - upMap["access_key"] = req.AccessKey - upMap["credential"] = req.Credential - upMap["backup_path"] = req.BackupPath - upMap["vars"] = req.Vars - backup.Bucket = req.Bucket - backup.Vars = req.Vars - backup.Credential = req.Credential - backup.AccessKey = req.AccessKey - backup.BackupPath = req.BackupPath - - if req.Type == constant.OneDrive { - if err := u.loadAccessToken(&backup); err != nil { - return err - } - upMap["credential"] = backup.Credential - upMap["vars"] = backup.Vars - } - if backup.Type != "LOCAL" { - isOk, err := u.checkBackupConn(&backup) - if err != nil || !isOk { - return buserr.WithMap("ErrBackupCheck", map[string]interface{}{"err": err.Error()}, err) - } - } - - if err := backupRepo.Update(req.ID, upMap); err != nil { - return err - } - if backup.Type == "LOCAL" { - if dir, ok := varMap["dir"]; ok { - if dirStr, isStr := dir.(string); isStr { - if strings.HasSuffix(dirStr, "/") && dirStr != "/" { - dirStr = dirStr[:strings.LastIndex(dirStr, "/")] - } - if err := copyDir(oldDir, dirStr); err != nil { - _ = backupRepo.Update(req.ID, map[string]interface{}{"vars": oldVars}) - return err - } - global.CONF.System.Backup = dirStr - } - } - } - return nil -} - -func (u *BackupService) ListFiles(req dto.BackupSearchFile) []string { - var datas []string - backup, err := backupRepo.Get(backupRepo.WithByType(req.Type)) - if err != nil { - return datas - } - client, err := u.NewClient(&backup) - if err != nil { - return datas - } - prefix := "system_snapshot" - if len(backup.BackupPath) != 0 { - prefix = path.Join(strings.TrimPrefix(backup.BackupPath, "/"), prefix) - } - files, err := client.ListObjects(prefix) - if err != nil { - global.LOG.Debugf("load files from %s failed, err: %v", req.Type, err) - return datas - } - for _, file := range files { - if len(file) != 0 { - datas = append(datas, path.Base(file)) - } - } - return datas -} - -func (u *BackupService) NewClient(backup *model.BackupAccount) (cloud_storage.CloudStorageClient, error) { - varMap := make(map[string]interface{}) - if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil { - return nil, err - } - varMap["bucket"] = backup.Bucket - switch backup.Type { - case constant.Sftp, constant.WebDAV: - varMap["username"] = backup.AccessKey - varMap["password"] = backup.Credential - case constant.OSS, constant.S3, constant.MinIo, constant.Cos, constant.Kodo: - varMap["accessKey"] = backup.AccessKey - varMap["secretKey"] = backup.Credential - } - - backClient, err := cloud_storage.NewCloudStorageClient(backup.Type, varMap) - if err != nil { - return nil, err - } - - return backClient, nil -} - -func (u *BackupService) loadByType(accountType string, accounts []model.BackupAccount) dto.BackupInfo { - for _, account := range accounts { - if account.Type == accountType { - var item dto.BackupInfo - if err := copier.Copy(&item, &account); err != nil { - global.LOG.Errorf("copy backup account to dto backup info failed, err: %v", err) - } - if account.Type == constant.OneDrive { - varMap := make(map[string]interface{}) - if err := json.Unmarshal([]byte(item.Vars), &varMap); err != nil { - return dto.BackupInfo{Type: accountType} - } - delete(varMap, "refresh_token") - itemVars, _ := json.Marshal(varMap) - item.Vars = string(itemVars) - } - return item - } - } - return dto.BackupInfo{Type: accountType} -} - -func (u *BackupService) loadAccessToken(backup *model.BackupAccount) error { - varMap := make(map[string]interface{}) - if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil { - return fmt.Errorf("unmarshal backup vars failed, err: %v", err) - } - refreshToken, err := client.RefreshToken("authorization_code", "refreshToken", varMap) - if err != nil { - return err - } - delete(varMap, "code") - varMap["refresh_status"] = constant.StatusSuccess - varMap["refresh_time"] = time.Now().Format(constant.DateTimeLayout) - varMap["refresh_token"] = refreshToken - itemVars, err := json.Marshal(varMap) - if err != nil { - return fmt.Errorf("json marshal var map failed, err: %v", err) - } - backup.Vars = string(itemVars) - return nil -} - -func (u *BackupService) loadRecordSize(records []model.BackupRecord) ([]dto.BackupRecords, error) { - var datas []dto.BackupRecords - clientMap := make(map[string]loadSizeHelper) - var wg sync.WaitGroup - for i := 0; i < len(records); i++ { - var item dto.BackupRecords - if err := copier.Copy(&item, &records[i]); err != nil { - return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - itemPath := path.Join(records[i].FileDir, records[i].FileName) - if _, ok := clientMap[records[i].Source]; !ok { - backup, err := backupRepo.Get(commonRepo.WithByType(records[i].Source)) - if err != nil { - global.LOG.Errorf("load backup model %s from db failed, err: %v", records[i].Source, err) - clientMap[records[i].Source] = loadSizeHelper{} - datas = append(datas, item) - continue - } - client, err := u.NewClient(&backup) - if err != nil { - global.LOG.Errorf("load backup client %s from db failed, err: %v", records[i].Source, err) - clientMap[records[i].Source] = loadSizeHelper{} - datas = append(datas, item) - continue - } - item.Size, _ = client.Size(path.Join(strings.TrimLeft(backup.BackupPath, "/"), itemPath)) - datas = append(datas, item) - clientMap[records[i].Source] = loadSizeHelper{backupPath: strings.TrimLeft(backup.BackupPath, "/"), client: client, isOk: true} - continue - } - if clientMap[records[i].Source].isOk { - wg.Add(1) - go func(index int) { - item.Size, _ = clientMap[records[index].Source].client.Size(path.Join(clientMap[records[index].Source].backupPath, itemPath)) - datas = append(datas, item) - wg.Done() - }(i) - } else { - datas = append(datas, item) - } - } - wg.Wait() - return datas, nil -} - -func loadLocalDir() (string, error) { - backup, err := backupRepo.Get(commonRepo.WithByType("LOCAL")) - if err != nil { - return "", err - } - varMap := make(map[string]interface{}) - if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil { - return "", err - } - if _, ok := varMap["dir"]; !ok { - return "", errors.New("load local backup dir failed") - } - baseDir, ok := varMap["dir"].(string) - if ok { - if _, err := os.Stat(baseDir); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(baseDir, os.ModePerm); err != nil { - return "", fmt.Errorf("mkdir %s failed, err: %v", baseDir, err) - } - } - return baseDir, nil - } - return "", fmt.Errorf("error type dir: %T", varMap["dir"]) -} - -func copyDir(src, dst string) error { - srcInfo, err := os.Stat(src) - if err != nil { - return err - } - if err = os.MkdirAll(dst, srcInfo.Mode()); err != nil { - return err - } - files, err := os.ReadDir(src) - if err != nil { - return err - } - - fileOP := fileUtils.NewFileOp() - for _, file := range files { - srcPath := fmt.Sprintf("%s/%s", src, file.Name()) - dstPath := fmt.Sprintf("%s/%s", dst, file.Name()) - if file.IsDir() { - if err = copyDir(srcPath, dstPath); err != nil { - global.LOG.Errorf("copy dir %s to %s failed, err: %v", srcPath, dstPath, err) - } - } else { - if err := fileOP.CopyFile(srcPath, dst); err != nil { - global.LOG.Errorf("copy file %s to %s failed, err: %v", srcPath, dstPath, err) - } - } - } - - return nil -} - -func (u *BackupService) checkBackupConn(backup *model.BackupAccount) (bool, error) { - client, err := u.NewClient(backup) - if err != nil { - return false, err - } - fileItem := path.Join(global.CONF.System.TmpDir, "test", "1panel") - if _, err := os.Stat(path.Dir(fileItem)); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(path.Dir(fileItem), os.ModePerm); err != nil { - return false, err - } - } - file, err := os.OpenFile(fileItem, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - return false, err - } - defer file.Close() - write := bufio.NewWriter(file) - _, _ = write.WriteString("1Panel 备份账号测试文件。\n") - _, _ = write.WriteString("1Panel 備份賬號測試文件。\n") - _, _ = write.WriteString("1Panel Backs up account test files.\n") - _, _ = write.WriteString("1Panelアカウントのテストファイルをバックアップします。\n") - write.Flush() - - targetPath := strings.TrimPrefix(path.Join(backup.BackupPath, "test/1panel"), "/") - return client.Upload(fileItem, targetPath) -} - -func StartRefreshOneDriveToken() { - service := NewIBackupService() - oneDriveCronID, err := global.Cron.AddJob("0 3 */31 * *", service) - if err != nil { - global.LOG.Errorf("can not add OneDrive corn job: %s", err.Error()) - return - } - global.OneDriveCronID = oneDriveCronID -} - -func (u *BackupService) Run() { - var backupItem model.BackupAccount - _ = global.DB.Where("`type` = ?", "OneDrive").First(&backupItem) - if backupItem.ID == 0 { - return - } - global.LOG.Info("start to refresh token of OneDrive ...") - varMap := make(map[string]interface{}) - if err := json.Unmarshal([]byte(backupItem.Vars), &varMap); err != nil { - global.LOG.Errorf("Failed to refresh OneDrive token, please retry, err: %v", err) - return - } - refreshToken, err := client.RefreshToken("refresh_token", "refreshToken", varMap) - varMap["refresh_status"] = constant.StatusSuccess - varMap["refresh_time"] = time.Now().Format(constant.DateTimeLayout) - if err != nil { - varMap["refresh_status"] = constant.StatusFailed - varMap["refresh_msg"] = err.Error() - global.LOG.Errorf("Failed to refresh OneDrive token, please retry, err: %v", err) - return - } - varMap["refresh_token"] = refreshToken - - varsItem, _ := json.Marshal(varMap) - _ = global.DB.Model(&model.BackupAccount{}). - Where("id = ?", backupItem.ID). - Updates(map[string]interface{}{ - "vars": varsItem, - }).Error - global.LOG.Info("Successfully refreshed OneDrive token.") -} diff --git a/backend/app/service/backup_app.go b/backend/app/service/backup_app.go deleted file mode 100644 index 2a1e0ccaf..000000000 --- a/backend/app/service/backup_app.go +++ /dev/null @@ -1,315 +0,0 @@ -package service - -import ( - "context" - "encoding/json" - "fmt" - "io/fs" - "os" - "path" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/buserr" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/1Panel-dev/1Panel/backend/utils/compose" - "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/pkg/errors" -) - -func (u *BackupService) AppBackup(req dto.CommonBackup) (*model.BackupRecord, error) { - localDir, err := loadLocalDir() - if err != nil { - return nil, err - } - app, err := appRepo.GetFirst(appRepo.WithKey(req.Name)) - if err != nil { - return nil, err - } - install, err := appInstallRepo.GetFirst(commonRepo.WithByName(req.DetailName), appInstallRepo.WithAppId(app.ID)) - if err != nil { - return nil, err - } - timeNow := time.Now().Format(constant.DateTimeSlimLayout) - itemDir := fmt.Sprintf("app/%s/%s", req.Name, req.DetailName) - backupDir := path.Join(localDir, itemDir) - - fileName := fmt.Sprintf("%s_%s.tar.gz", req.DetailName, timeNow+common.RandStrAndNum(5)) - if err := handleAppBackup(&install, backupDir, fileName, "", req.Secret); err != nil { - return nil, err - } - - record := &model.BackupRecord{ - Type: "app", - Name: req.Name, - DetailName: req.DetailName, - Source: "LOCAL", - BackupType: "LOCAL", - FileDir: itemDir, - FileName: fileName, - } - - if err := backupRepo.CreateRecord(record); err != nil { - global.LOG.Errorf("save backup record failed, err: %v", err) - return nil, err - } - return record, nil -} - -func (u *BackupService) AppRecover(req dto.CommonRecover) error { - app, err := appRepo.GetFirst(appRepo.WithKey(req.Name)) - if err != nil { - return err - } - install, err := appInstallRepo.GetFirst(commonRepo.WithByName(req.DetailName), appInstallRepo.WithAppId(app.ID)) - if err != nil { - return err - } - - fileOp := files.NewFileOp() - if !fileOp.Stat(req.File) { - return buserr.WithName("ErrFileNotFound", req.File) - } - if _, err := compose.Down(install.GetComposePath()); err != nil { - return err - } - if err := handleAppRecover(&install, req.File, false, req.Secret); err != nil { - return err - } - return nil -} - -func handleAppBackup(install *model.AppInstall, backupDir, fileName string, excludes string, secret string) error { - fileOp := files.NewFileOp() - tmpDir := fmt.Sprintf("%s/%s", backupDir, strings.ReplaceAll(fileName, ".tar.gz", "")) - if !fileOp.Stat(tmpDir) { - if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil { - return fmt.Errorf("mkdir %s failed, err: %v", backupDir, err) - } - } - defer func() { - _ = os.RemoveAll(tmpDir) - }() - - remarkInfo, _ := json.Marshal(install) - remarkInfoPath := fmt.Sprintf("%s/app.json", tmpDir) - if err := fileOp.SaveFile(remarkInfoPath, string(remarkInfo), fs.ModePerm); err != nil { - return err - } - - appPath := install.GetPath() - if err := handleTar(appPath, tmpDir, "app.tar.gz", excludes, ""); err != nil { - return err - } - - resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID)) - for _, resource := range resources { - switch resource.Key { - case constant.AppMysql, constant.AppMariaDB: - db, err := mysqlRepo.Get(commonRepo.WithByID(resource.ResourceId)) - if err != nil { - return err - } - if err := handleMysqlBackup(db.MysqlName, resource.Key, db.Name, tmpDir, fmt.Sprintf("%s.sql.gz", install.Name)); err != nil { - return err - } - case constant.AppPostgresql: - db, err := postgresqlRepo.Get(commonRepo.WithByID(resource.ResourceId)) - if err != nil { - return err - } - if err := handlePostgresqlBackup(db.PostgresqlName, db.Name, tmpDir, fmt.Sprintf("%s.sql.gz", install.Name)); err != nil { - return err - } - } - } - - if err := handleTar(tmpDir, backupDir, fileName, "", secret); err != nil { - return err - } - return nil -} - -func handleAppRecover(install *model.AppInstall, recoverFile string, isRollback bool, secret string) error { - isOk := false - fileOp := files.NewFileOp() - if err := handleUnTar(recoverFile, path.Dir(recoverFile), secret); err != nil { - return err - } - tmpPath := strings.ReplaceAll(recoverFile, ".tar.gz", "") - defer func() { - _, _ = compose.Up(install.GetComposePath()) - _ = os.RemoveAll(strings.ReplaceAll(recoverFile, ".tar.gz", "")) - }() - - if !fileOp.Stat(tmpPath+"/app.json") || !fileOp.Stat(tmpPath+"/app.tar.gz") { - return errors.New("the wrong recovery package does not have app.json or app.tar.gz files") - } - var oldInstall model.AppInstall - appjson, err := os.ReadFile(tmpPath + "/app.json") - if err != nil { - return err - } - if err := json.Unmarshal(appjson, &oldInstall); err != nil { - return fmt.Errorf("unmarshal app.json failed, err: %v", err) - } - if oldInstall.App.Key != install.App.Key || oldInstall.Name != install.Name { - return errors.New("the current backup file does not match the application") - } - - if !isRollback { - rollbackFile := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("app/%s_%s.tar.gz", install.Name, time.Now().Format(constant.DateTimeSlimLayout))) - if err := handleAppBackup(install, path.Dir(rollbackFile), path.Base(rollbackFile), "", ""); err != nil { - return fmt.Errorf("backup app %s for rollback before recover failed, err: %v", install.Name, err) - } - defer func() { - if !isOk { - global.LOG.Info("recover failed, start to rollback now") - if err := handleAppRecover(install, rollbackFile, true, secret); err != nil { - global.LOG.Errorf("rollback app %s from %s failed, err: %v", install.Name, rollbackFile, err) - return - } - global.LOG.Infof("rollback app %s from %s successful", install.Name, rollbackFile) - _ = os.RemoveAll(rollbackFile) - } else { - _ = os.RemoveAll(rollbackFile) - } - }() - } - - newEnvFile := "" - resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID)) - for _, resource := range resources { - var database model.Database - switch resource.From { - case constant.AppResourceRemote: - database, err = databaseRepo.Get(commonRepo.WithByID(resource.LinkId)) - if err != nil { - return err - } - case constant.AppResourceLocal: - resourceApp, err := appInstallRepo.GetFirst(commonRepo.WithByID(resource.LinkId)) - if err != nil { - return err - } - database, err = databaseRepo.Get(databaseRepo.WithAppInstallID(resourceApp.ID), commonRepo.WithByType(resource.Key), databaseRepo.WithByFrom(constant.AppResourceLocal), commonRepo.WithByName(resourceApp.Name)) - if err != nil { - return err - } - } - switch database.Type { - case constant.AppPostgresql: - db, err := postgresqlRepo.Get(commonRepo.WithByID(resource.ResourceId)) - if err != nil { - return err - } - if err := handlePostgresqlRecover(dto.CommonRecover{ - Name: database.Name, - DetailName: db.Name, - File: fmt.Sprintf("%s/%s.sql.gz", tmpPath, install.Name), - }, true); err != nil { - global.LOG.Errorf("handle recover from sql.gz failed, err: %v", err) - return err - } - case constant.AppMysql, constant.AppMariaDB: - db, err := mysqlRepo.Get(commonRepo.WithByID(resource.ResourceId)) - if err != nil { - return err - } - newDB, envMap, err := reCreateDB(db.ID, database, oldInstall.Env) - if err != nil { - return err - } - oldHost := fmt.Sprintf("\"PANEL_DB_HOST\":\"%v\"", envMap["PANEL_DB_HOST"].(string)) - newHost := fmt.Sprintf("\"PANEL_DB_HOST\":\"%v\"", database.Address) - oldInstall.Env = strings.ReplaceAll(oldInstall.Env, oldHost, newHost) - envMap["PANEL_DB_HOST"] = database.Address - newEnvFile, err = coverEnvJsonToStr(oldInstall.Env) - if err != nil { - return err - } - _ = appInstallResourceRepo.BatchUpdateBy(map[string]interface{}{"resource_id": newDB.ID}, commonRepo.WithByID(resource.ID)) - - if err := handleMysqlRecover(dto.CommonRecover{ - Name: newDB.MysqlName, - DetailName: newDB.Name, - File: fmt.Sprintf("%s/%s.sql.gz", tmpPath, install.Name), - }, true); err != nil { - global.LOG.Errorf("handle recover from sql.gz failed, err: %v", err) - return err - } - } - } - - appDir := install.GetPath() - backPath := fmt.Sprintf("%s_bak", appDir) - _ = fileOp.Rename(appDir, backPath) - _ = fileOp.CreateDir(appDir, 0755) - - if err := handleUnTar(tmpPath+"/app.tar.gz", install.GetAppPath(), ""); err != nil { - global.LOG.Errorf("handle recover from app.tar.gz failed, err: %v", err) - _ = fileOp.DeleteDir(appDir) - _ = fileOp.Rename(backPath, appDir) - return err - } - _ = fileOp.DeleteDir(backPath) - - if len(newEnvFile) != 0 { - envPath := fmt.Sprintf("%s/%s/.env", install.GetAppPath(), install.Name) - file, err := os.OpenFile(envPath, os.O_WRONLY|os.O_TRUNC, 0640) - if err != nil { - return err - } - defer file.Close() - _, _ = file.WriteString(newEnvFile) - } - - oldInstall.ID = install.ID - oldInstall.Status = constant.StatusRunning - oldInstall.AppId = install.AppId - oldInstall.AppDetailId = install.AppDetailId - oldInstall.App.ID = install.AppId - if err := appInstallRepo.Save(context.Background(), &oldInstall); err != nil { - global.LOG.Errorf("save db app install failed, err: %v", err) - return err - } - isOk = true - - return nil -} - -func reCreateDB(dbID uint, database model.Database, oldEnv string) (*model.DatabaseMysql, map[string]interface{}, error) { - mysqlService := NewIMysqlService() - ctx := context.Background() - _ = mysqlService.Delete(ctx, dto.MysqlDBDelete{ID: dbID, Database: database.Name, Type: database.Type, DeleteBackup: false, ForceDelete: true}) - - envMap := make(map[string]interface{}) - if err := json.Unmarshal([]byte(oldEnv), &envMap); err != nil { - return nil, envMap, err - } - oldName, _ := envMap["PANEL_DB_NAME"].(string) - oldUser, _ := envMap["PANEL_DB_USER"].(string) - oldPassword, _ := envMap["PANEL_DB_USER_PASSWORD"].(string) - createDB, err := mysqlService.Create(context.Background(), dto.MysqlDBCreate{ - Name: oldName, - From: database.From, - Database: database.Name, - Format: "utf8mb4", - Username: oldUser, - Password: oldPassword, - Permission: "%", - }) - cronjobs, _ := cronjobRepo.List(cronjobRepo.WithByDbName(fmt.Sprintf("%v", dbID))) - for _, job := range cronjobs { - _ = cronjobRepo.Update(job.ID, map[string]interface{}{"db_name": fmt.Sprintf("%v", createDB.ID)}) - } - if err != nil { - return nil, envMap, err - } - return createDB, envMap, nil -} diff --git a/backend/app/service/backup_mysql.go b/backend/app/service/backup_mysql.go deleted file mode 100644 index 40a1d90ce..000000000 --- a/backend/app/service/backup_mysql.go +++ /dev/null @@ -1,193 +0,0 @@ -package service - -import ( - "fmt" - "github.com/1Panel-dev/1Panel/backend/constant" - "os" - "path" - "path/filepath" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/buserr" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/1Panel-dev/1Panel/backend/utils/mysql/client" -) - -func (u *BackupService) MysqlBackup(req dto.CommonBackup) error { - localDir, err := loadLocalDir() - if err != nil { - return err - } - - timeNow := time.Now().Format(constant.DateTimeSlimLayout) - itemDir := fmt.Sprintf("database/%s/%s/%s", req.Type, req.Name, req.DetailName) - targetDir := path.Join(localDir, itemDir) - fileName := fmt.Sprintf("%s_%s.sql.gz", req.DetailName, timeNow+common.RandStrAndNum(5)) - - if err := handleMysqlBackup(req.Name, req.Type, req.DetailName, targetDir, fileName); err != nil { - return err - } - - record := &model.BackupRecord{ - Type: req.Type, - Name: req.Name, - DetailName: req.DetailName, - Source: "LOCAL", - BackupType: "LOCAL", - FileDir: itemDir, - FileName: fileName, - } - if err := backupRepo.CreateRecord(record); err != nil { - global.LOG.Errorf("save backup record failed, err: %v", err) - } - return nil -} - -func (u *BackupService) MysqlRecover(req dto.CommonRecover) error { - if err := handleMysqlRecover(req, false); err != nil { - return err - } - return nil -} - -func (u *BackupService) MysqlRecoverByUpload(req dto.CommonRecover) error { - file := req.File - fileName := path.Base(req.File) - if strings.HasSuffix(fileName, ".tar.gz") { - fileNameItem := time.Now().Format(constant.DateTimeSlimLayout) - dstDir := fmt.Sprintf("%s/%s", path.Dir(req.File), fileNameItem) - if _, err := os.Stat(dstDir); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(dstDir, os.ModePerm); err != nil { - return fmt.Errorf("mkdir %s failed, err: %v", dstDir, err) - } - } - if err := handleUnTar(req.File, dstDir, ""); err != nil { - _ = os.RemoveAll(dstDir) - return err - } - global.LOG.Infof("decompress file %s successful, now start to check test.sql is exist", req.File) - hasTestSql := false - _ = filepath.Walk(dstDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return nil - } - if !info.IsDir() && info.Name() == "test.sql" { - hasTestSql = true - file = path - fileName = "test.sql" - } - return nil - }) - if !hasTestSql { - _ = os.RemoveAll(dstDir) - return fmt.Errorf("no such file named test.sql in %s", fileName) - } - defer func() { - _ = os.RemoveAll(dstDir) - }() - } - - req.File = path.Dir(file) + "/" + fileName - if err := handleMysqlRecover(req, false); err != nil { - return err - } - global.LOG.Info("recover from uploads successful!") - return nil -} - -func handleMysqlBackup(database, dbType, dbName, targetDir, fileName string) error { - dbInfo, err := mysqlRepo.Get(commonRepo.WithByName(dbName), mysqlRepo.WithByMysqlName(database)) - if err != nil { - return err - } - cli, version, err := LoadMysqlClientByFrom(database) - if err != nil { - return err - } - - backupInfo := client.BackupInfo{ - Name: dbName, - Type: dbType, - Version: version, - Format: dbInfo.Format, - TargetDir: targetDir, - FileName: fileName, - - Timeout: 300, - } - if err := cli.Backup(backupInfo); err != nil { - return err - } - return nil -} - -func handleMysqlRecover(req dto.CommonRecover, isRollback bool) error { - isOk := false - fileOp := files.NewFileOp() - if !fileOp.Stat(req.File) { - return buserr.WithName("ErrFileNotFound", req.File) - } - dbInfo, err := mysqlRepo.Get(commonRepo.WithByName(req.DetailName), mysqlRepo.WithByMysqlName(req.Name)) - if err != nil { - return err - } - cli, version, err := LoadMysqlClientByFrom(req.Name) - if err != nil { - return err - } - - if !isRollback { - rollbackFile := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("database/%s/%s_%s.sql.gz", req.Type, req.DetailName, time.Now().Format(constant.DateTimeSlimLayout))) - if err := cli.Backup(client.BackupInfo{ - Name: req.DetailName, - Type: req.Type, - Version: version, - Format: dbInfo.Format, - TargetDir: path.Dir(rollbackFile), - FileName: path.Base(rollbackFile), - - Timeout: 300, - }); err != nil { - return fmt.Errorf("backup mysql db %s for rollback before recover failed, err: %v", req.DetailName, err) - } - defer func() { - if !isOk { - global.LOG.Info("recover failed, start to rollback now") - if err := cli.Recover(client.RecoverInfo{ - Name: req.DetailName, - Type: req.Type, - Version: version, - Format: dbInfo.Format, - SourceFile: rollbackFile, - - Timeout: 300, - }); err != nil { - global.LOG.Errorf("rollback mysql db %s from %s failed, err: %v", req.DetailName, rollbackFile, err) - } - global.LOG.Infof("rollback mysql db %s from %s successful", req.DetailName, rollbackFile) - _ = os.RemoveAll(rollbackFile) - } else { - _ = os.RemoveAll(rollbackFile) - } - }() - } - if err := cli.Recover(client.RecoverInfo{ - Name: req.DetailName, - Type: req.Type, - Version: version, - Format: dbInfo.Format, - SourceFile: req.File, - - Timeout: 300, - }); err != nil { - return err - } - isOk = true - return nil -} diff --git a/backend/app/service/backup_postgresql.go b/backend/app/service/backup_postgresql.go deleted file mode 100644 index ee2d7852f..000000000 --- a/backend/app/service/backup_postgresql.go +++ /dev/null @@ -1,178 +0,0 @@ -package service - -import ( - "fmt" - "github.com/1Panel-dev/1Panel/backend/constant" - "os" - "path" - "path/filepath" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/utils/common" - pgclient "github.com/1Panel-dev/1Panel/backend/utils/postgresql/client" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/1Panel-dev/1Panel/backend/utils/postgresql/client" -) - -func (u *BackupService) PostgresqlBackup(req dto.CommonBackup) error { - localDir, err := loadLocalDir() - if err != nil { - return err - } - - timeNow := time.Now().Format(constant.DateTimeSlimLayout) - itemDir := fmt.Sprintf("database/%s/%s/%s", req.Type, req.Name, req.DetailName) - targetDir := path.Join(localDir, itemDir) - fileName := fmt.Sprintf("%s_%s.sql.gz", req.DetailName, timeNow+common.RandStrAndNum(5)) - - if err := handlePostgresqlBackup(req.Name, req.DetailName, targetDir, fileName); err != nil { - return err - } - - record := &model.BackupRecord{ - Type: req.Type, - Name: req.Name, - DetailName: req.DetailName, - Source: "LOCAL", - BackupType: "LOCAL", - FileDir: itemDir, - FileName: fileName, - } - if err := backupRepo.CreateRecord(record); err != nil { - global.LOG.Errorf("save backup record failed, err: %v", err) - } - return nil -} -func (u *BackupService) PostgresqlRecover(req dto.CommonRecover) error { - if err := handlePostgresqlRecover(req, false); err != nil { - return err - } - return nil -} - -func (u *BackupService) PostgresqlRecoverByUpload(req dto.CommonRecover) error { - file := req.File - fileName := path.Base(req.File) - if strings.HasSuffix(fileName, ".tar.gz") { - fileNameItem := time.Now().Format(constant.DateTimeSlimLayout) - dstDir := fmt.Sprintf("%s/%s", path.Dir(req.File), fileNameItem) - if _, err := os.Stat(dstDir); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(dstDir, os.ModePerm); err != nil { - return fmt.Errorf("mkdir %s failed, err: %v", dstDir, err) - } - } - if err := handleUnTar(req.File, dstDir, ""); err != nil { - _ = os.RemoveAll(dstDir) - return err - } - global.LOG.Infof("decompress file %s successful, now start to check test.sql is exist", req.File) - hasTestSql := false - _ = filepath.Walk(dstDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return nil - } - if !info.IsDir() && info.Name() == "test.sql" { - hasTestSql = true - file = path - fileName = "test.sql" - } - return nil - }) - if !hasTestSql { - _ = os.RemoveAll(dstDir) - return fmt.Errorf("no such file named test.sql in %s", fileName) - } - defer func() { - _ = os.RemoveAll(dstDir) - }() - } - - req.File = path.Dir(file) + "/" + fileName - if err := handlePostgresqlRecover(req, false); err != nil { - return err - } - global.LOG.Info("recover from uploads successful!") - return nil -} -func handlePostgresqlBackup(database, dbName, targetDir, fileName string) error { - cli, err := LoadPostgresqlClientByFrom(database) - if err != nil { - return err - } - defer cli.Close() - - backupInfo := pgclient.BackupInfo{ - Name: dbName, - TargetDir: targetDir, - FileName: fileName, - - Timeout: 300, - } - if err := cli.Backup(backupInfo); err != nil { - return err - } - return nil -} - -func handlePostgresqlRecover(req dto.CommonRecover, isRollback bool) error { - isOk := false - fileOp := files.NewFileOp() - if !fileOp.Stat(req.File) { - return buserr.WithName("ErrFileNotFound", req.File) - } - dbInfo, err := postgresqlRepo.Get(commonRepo.WithByName(req.DetailName), postgresqlRepo.WithByPostgresqlName(req.Name)) - if err != nil { - return err - } - cli, err := LoadPostgresqlClientByFrom(req.Name) - if err != nil { - return err - } - defer cli.Close() - - if !isRollback { - rollbackFile := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("database/%s/%s_%s.sql.gz", req.Type, req.DetailName, time.Now().Format(constant.DateTimeSlimLayout))) - if err := cli.Backup(client.BackupInfo{ - Name: req.DetailName, - TargetDir: path.Dir(rollbackFile), - FileName: path.Base(rollbackFile), - - Timeout: 300, - }); err != nil { - return fmt.Errorf("backup postgresql db %s for rollback before recover failed, err: %v", req.DetailName, err) - } - defer func() { - if !isOk { - global.LOG.Info("recover failed, start to rollback now") - if err := cli.Recover(client.RecoverInfo{ - Name: req.DetailName, - SourceFile: rollbackFile, - - Timeout: 300, - }); err != nil { - global.LOG.Errorf("rollback postgresql db %s from %s failed, err: %v", req.DetailName, rollbackFile, err) - } - global.LOG.Infof("rollback postgresql db %s from %s successful", req.DetailName, rollbackFile) - _ = os.RemoveAll(rollbackFile) - } else { - _ = os.RemoveAll(rollbackFile) - } - }() - } - if err := cli.Recover(client.RecoverInfo{ - Name: req.DetailName, - SourceFile: req.File, - Username: dbInfo.Username, - Timeout: 300, - }); err != nil { - return err - } - isOk = true - return nil -} diff --git a/backend/app/service/backup_redis.go b/backend/app/service/backup_redis.go deleted file mode 100644 index ddec5303a..000000000 --- a/backend/app/service/backup_redis.go +++ /dev/null @@ -1,194 +0,0 @@ -package service - -import ( - "fmt" - "os" - "path" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/app/repo" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/1Panel-dev/1Panel/backend/utils/compose" - "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/pkg/errors" -) - -func (u *BackupService) RedisBackup(db dto.CommonBackup) error { - localDir, err := loadLocalDir() - if err != nil { - return err - } - redisInfo, err := appInstallRepo.LoadBaseInfo("redis", db.Name) - if err != nil { - return err - } - appendonly, err := configGetStr(redisInfo.ContainerName, redisInfo.Password, "appendonly") - if err != nil { - return err - } - global.LOG.Infof("appendonly in redis conf is %s", appendonly) - - timeNow := time.Now().Format(constant.DateTimeSlimLayout) + common.RandStrAndNum(5) - fileName := fmt.Sprintf("%s.rdb", timeNow) - if appendonly == "yes" { - if strings.HasPrefix(redisInfo.Version, "6.") { - fileName = fmt.Sprintf("%s.aof", timeNow) - } else { - fileName = fmt.Sprintf("%s.tar.gz", timeNow) - } - } - itemDir := fmt.Sprintf("database/redis/%s", redisInfo.Name) - backupDir := path.Join(localDir, itemDir) - if err := handleRedisBackup(redisInfo, backupDir, fileName, db.Secret); err != nil { - return err - } - record := &model.BackupRecord{ - Type: "redis", - Name: db.Name, - Source: "LOCAL", - BackupType: "LOCAL", - FileDir: itemDir, - FileName: fileName, - } - if err := backupRepo.CreateRecord(record); err != nil { - global.LOG.Errorf("save backup record failed, err: %v", err) - } - - return nil -} - -func (u *BackupService) RedisRecover(req dto.CommonRecover) error { - redisInfo, err := appInstallRepo.LoadBaseInfo("redis", req.Name) - if err != nil { - return err - } - global.LOG.Infof("recover redis from backup file %s", req.File) - if err := handleRedisRecover(redisInfo, req.File, false, req.Secret); err != nil { - return err - } - return nil -} - -func handleRedisBackup(redisInfo *repo.RootInfo, backupDir, fileName string, secret string) error { - fileOp := files.NewFileOp() - if !fileOp.Stat(backupDir) { - if err := os.MkdirAll(backupDir, os.ModePerm); err != nil { - return fmt.Errorf("mkdir %s failed, err: %v", backupDir, err) - } - } - - stdout, err := cmd.Execf("docker exec %s redis-cli -a %s --no-auth-warning save", redisInfo.ContainerName, redisInfo.Password) - if err != nil { - return errors.New(string(stdout)) - } - - if strings.HasSuffix(fileName, ".tar.gz") { - redisDataDir := fmt.Sprintf("%s/%s/%s/data/appendonlydir", constant.AppInstallDir, "redis", redisInfo.Name) - if err := handleTar(redisDataDir, backupDir, fileName, "", secret); err != nil { - return err - } - return nil - } - if strings.HasSuffix(fileName, ".aof") { - stdout1, err := cmd.Execf("docker cp %s:/data/appendonly.aof %s/%s", redisInfo.ContainerName, backupDir, fileName) - if err != nil { - return errors.New(string(stdout1)) - } - return nil - } - - stdout1, err1 := cmd.Execf("docker cp %s:/data/dump.rdb %s/%s", redisInfo.ContainerName, backupDir, fileName) - if err1 != nil { - return errors.New(string(stdout1)) - } - return nil -} - -func handleRedisRecover(redisInfo *repo.RootInfo, recoverFile string, isRollback bool, secret string) error { - fileOp := files.NewFileOp() - if !fileOp.Stat(recoverFile) { - return buserr.WithName("ErrFileNotFound", recoverFile) - } - - appendonly, err := configGetStr(redisInfo.ContainerName, redisInfo.Password, "appendonly") - if err != nil { - return err - } - - if appendonly == "yes" { - if strings.HasPrefix(redisInfo.Version, "6.") && !strings.HasSuffix(recoverFile, ".aof") { - return buserr.New(constant.ErrTypeOfRedis) - } - if strings.HasPrefix(redisInfo.Version, "7.") && !strings.HasSuffix(recoverFile, ".tar.gz") { - return buserr.New(constant.ErrTypeOfRedis) - } - } else { - if !strings.HasSuffix(recoverFile, ".rdb") { - return buserr.New(constant.ErrTypeOfRedis) - } - } - - global.LOG.Infof("appendonly in redis conf is %s", appendonly) - isOk := false - if !isRollback { - suffix := "rdb" - if appendonly == "yes" { - if strings.HasPrefix(redisInfo.Version, "6.") { - suffix = "aof" - } else { - suffix = "tar.gz" - } - } - rollbackFile := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("database/redis/%s_%s.%s", redisInfo.Name, time.Now().Format(constant.DateTimeSlimLayout), suffix)) - if err := handleRedisBackup(redisInfo, path.Dir(rollbackFile), path.Base(rollbackFile), secret); err != nil { - return fmt.Errorf("backup database %s for rollback before recover failed, err: %v", redisInfo.Name, err) - } - defer func() { - if !isOk { - global.LOG.Info("recover failed, start to rollback now") - if err := handleRedisRecover(redisInfo, rollbackFile, true, secret); err != nil { - global.LOG.Errorf("rollback redis from %s failed, err: %v", rollbackFile, err) - return - } - global.LOG.Infof("rollback redis from %s successful", rollbackFile) - _ = os.RemoveAll(rollbackFile) - } else { - _ = os.RemoveAll(rollbackFile) - } - }() - } - composeDir := fmt.Sprintf("%s/redis/%s", constant.AppInstallDir, redisInfo.Name) - if _, err := compose.Down(composeDir + "/docker-compose.yml"); err != nil { - return err - } - if appendonly == "yes" && strings.HasPrefix(redisInfo.Version, "7.") { - redisDataDir := fmt.Sprintf("%s/%s/%s/data", constant.AppInstallDir, "redis", redisInfo.Name) - if err := handleUnTar(recoverFile, redisDataDir, secret); err != nil { - return err - } - } else { - itemName := "dump.rdb" - if appendonly == "yes" && strings.HasPrefix(redisInfo.Version, "6.") { - itemName = "appendonly.aof" - } - input, err := os.ReadFile(recoverFile) - if err != nil { - return err - } - if err = os.WriteFile(composeDir+"/data/"+itemName, input, 0640); err != nil { - return err - } - } - if _, err := compose.Up(composeDir + "/docker-compose.yml"); err != nil { - return err - } - isOk = true - return nil -} diff --git a/backend/app/service/backup_runtime.go b/backend/app/service/backup_runtime.go deleted file mode 100644 index cf0fd510b..000000000 --- a/backend/app/service/backup_runtime.go +++ /dev/null @@ -1,129 +0,0 @@ -package service - -import ( - "encoding/json" - "errors" - "fmt" - "io/fs" - "os" - "path" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/files" -) - -func handleRuntimeBackup(runtime *model.Runtime, backupDir, fileName string, excludes string, secret string) error { - fileOp := files.NewFileOp() - tmpDir := fmt.Sprintf("%s/%s", backupDir, strings.ReplaceAll(fileName, ".tar.gz", "")) - if !fileOp.Stat(tmpDir) { - if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil { - return fmt.Errorf("mkdir %s failed, err: %v", backupDir, err) - } - } - defer func() { - _ = os.RemoveAll(tmpDir) - }() - - remarkInfo, _ := json.Marshal(runtime) - remarkInfoPath := fmt.Sprintf("%s/runtime.json", tmpDir) - if err := fileOp.SaveFile(remarkInfoPath, string(remarkInfo), fs.ModePerm); err != nil { - return err - } - - appPath := runtime.GetPath() - if err := handleTar(appPath, tmpDir, "runtime.tar.gz", excludes, secret); err != nil { - return err - } - if err := handleTar(tmpDir, backupDir, fileName, "", secret); err != nil { - return err - } - return nil -} - -func handleRuntimeRecover(runtime *model.Runtime, recoverFile string, isRollback bool, secret string) error { - isOk := false - fileOp := files.NewFileOp() - if err := handleUnTar(recoverFile, path.Dir(recoverFile), secret); err != nil { - return err - } - tmpPath := strings.ReplaceAll(recoverFile, ".tar.gz", "") - defer func() { - go startRuntime(runtime) - _ = os.RemoveAll(strings.ReplaceAll(recoverFile, ".tar.gz", "")) - }() - - if !fileOp.Stat(tmpPath+"/runtime.json") || !fileOp.Stat(tmpPath+"/runtime.tar.gz") { - return errors.New("the wrong recovery package does not have runtime.json or runtime.tar.gz files") - } - var oldRuntime model.Runtime - runtimeJson, err := os.ReadFile(tmpPath + "/runtime.json") - if err != nil { - return err - } - if err := json.Unmarshal(runtimeJson, &oldRuntime); err != nil { - return fmt.Errorf("unmarshal runtime.json failed, err: %v", err) - } - if oldRuntime.Type != runtime.Type || oldRuntime.Name != runtime.Name { - return errors.New("the current backup file does not match the application") - } - - if !isRollback { - rollbackFile := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("runtime/%s_%s.tar.gz", runtime.Name, time.Now().Format(constant.DateTimeSlimLayout))) - if err := handleRuntimeBackup(runtime, path.Dir(rollbackFile), path.Base(rollbackFile), "", secret); err != nil { - return fmt.Errorf("backup runtime %s for rollback before recover failed, err: %v", runtime.Name, err) - } - defer func() { - if !isOk { - global.LOG.Info("recover failed, start to rollback now") - if err := handleRuntimeRecover(runtime, rollbackFile, true, secret); err != nil { - global.LOG.Errorf("rollback runtime %s from %s failed, err: %v", runtime.Name, rollbackFile, err) - return - } - global.LOG.Infof("rollback runtime %s from %s successful", runtime.Name, rollbackFile) - _ = os.RemoveAll(rollbackFile) - } else { - _ = os.RemoveAll(rollbackFile) - } - }() - } - - newEnvFile, err := coverEnvJsonToStr(runtime.Env) - if err != nil { - return err - } - runtimeDir := runtime.GetPath() - backPath := fmt.Sprintf("%s_bak", runtimeDir) - _ = fileOp.Rename(runtimeDir, backPath) - _ = fileOp.CreateDir(runtimeDir, 0755) - - if err := handleUnTar(tmpPath+"/runtime.tar.gz", fmt.Sprintf("%s/%s", constant.RuntimeDir, runtime.Type), secret); err != nil { - global.LOG.Errorf("handle recover from runtime.tar.gz failed, err: %v", err) - _ = fileOp.DeleteDir(runtimeDir) - _ = fileOp.Rename(backPath, runtimeDir) - return err - } - _ = fileOp.DeleteDir(backPath) - - if len(newEnvFile) != 0 { - envPath := fmt.Sprintf("%s/%s/%s/.env", constant.RuntimeDir, runtime.Type, runtime.Name) - file, err := os.OpenFile(envPath, os.O_WRONLY|os.O_TRUNC, 0640) - if err != nil { - return err - } - defer file.Close() - _, _ = file.WriteString(newEnvFile) - } - - oldRuntime.ID = runtime.ID - oldRuntime.Status = constant.RuntimeStarting - if err := runtimeRepo.Save(&oldRuntime); err != nil { - global.LOG.Errorf("save db app install failed, err: %v", err) - return err - } - isOk = true - return nil -} diff --git a/backend/app/service/backup_website.go b/backend/app/service/backup_website.go deleted file mode 100644 index e7e7c3da1..000000000 --- a/backend/app/service/backup_website.go +++ /dev/null @@ -1,269 +0,0 @@ -package service - -import ( - "encoding/json" - "fmt" - "io/fs" - "os" - "path" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/1Panel-dev/1Panel/backend/utils/compose" - "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/pkg/errors" -) - -func (u *BackupService) WebsiteBackup(req dto.CommonBackup) error { - localDir, err := loadLocalDir() - if err != nil { - return err - } - website, err := websiteRepo.GetFirst(websiteRepo.WithAlias(req.DetailName)) - if err != nil { - return err - } - - timeNow := time.Now().Format(constant.DateTimeSlimLayout) - itemDir := fmt.Sprintf("website/%s", req.Name) - backupDir := path.Join(localDir, itemDir) - fileName := fmt.Sprintf("%s_%s.tar.gz", website.PrimaryDomain, timeNow+common.RandStrAndNum(5)) - if err := handleWebsiteBackup(&website, backupDir, fileName, "", req.Secret); err != nil { - return err - } - - record := &model.BackupRecord{ - Type: "website", - Name: website.PrimaryDomain, - DetailName: req.DetailName, - Source: "LOCAL", - BackupType: "LOCAL", - FileDir: itemDir, - FileName: fileName, - } - if err := backupRepo.CreateRecord(record); err != nil { - global.LOG.Errorf("save backup record failed, err: %v", err) - return err - } - return nil -} - -func (u *BackupService) WebsiteRecover(req dto.CommonRecover) error { - fileOp := files.NewFileOp() - if !fileOp.Stat(req.File) { - return buserr.WithName("ErrFileNotFound", req.File) - } - website, err := websiteRepo.GetFirst(websiteRepo.WithAlias(req.DetailName)) - if err != nil { - return err - } - global.LOG.Infof("recover website %s from backup file %s", req.Name, req.File) - if err := handleWebsiteRecover(&website, req.File, false, req.Secret); err != nil { - return err - } - return nil -} - -func handleWebsiteRecover(website *model.Website, recoverFile string, isRollback bool, secret string) error { - fileOp := files.NewFileOp() - tmpPath := strings.ReplaceAll(recoverFile, ".tar.gz", "") - if err := handleUnTar(recoverFile, path.Dir(recoverFile), secret); err != nil { - return err - } - defer func() { - _ = os.RemoveAll(tmpPath) - }() - - var oldWebsite model.Website - websiteJson, err := os.ReadFile(tmpPath + "/website.json") - if err != nil { - return err - } - if err := json.Unmarshal(websiteJson, &oldWebsite); err != nil { - return fmt.Errorf("unmarshal app.json failed, err: %v", err) - } - - if err := checkValidOfWebsite(&oldWebsite, website); err != nil { - return err - } - - temPathWithName := tmpPath + "/" + website.Alias - if !fileOp.Stat(tmpPath+"/website.json") || !fileOp.Stat(temPathWithName+".conf") || !fileOp.Stat(temPathWithName+".web.tar.gz") { - return buserr.WithDetail(constant.ErrBackupExist, ".conf or .web.tar.gz", nil) - } - if website.Type == constant.Deployment { - if !fileOp.Stat(temPathWithName + ".app.tar.gz") { - return buserr.WithDetail(constant.ErrBackupExist, ".app.tar.gz", nil) - } - } - - isOk := false - if !isRollback { - rollbackFile := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("website/%s_%s.tar.gz", website.Alias, time.Now().Format(constant.DateTimeSlimLayout))) - if err := handleWebsiteBackup(website, path.Dir(rollbackFile), path.Base(rollbackFile), "", ""); err != nil { - return fmt.Errorf("backup website %s for rollback before recover failed, err: %v", website.Alias, err) - } - defer func() { - if !isOk { - global.LOG.Info("recover failed, start to rollback now") - if err := handleWebsiteRecover(website, rollbackFile, true, ""); err != nil { - global.LOG.Errorf("rollback website %s from %s failed, err: %v", website.Alias, rollbackFile, err) - return - } - global.LOG.Infof("rollback website %s from %s successful", website.Alias, rollbackFile) - _ = os.RemoveAll(rollbackFile) - } else { - _ = os.RemoveAll(rollbackFile) - } - }() - } - - nginxInfo, err := appInstallRepo.LoadBaseInfo(constant.AppOpenresty, "") - if err != nil { - return err - } - nginxConfPath := fmt.Sprintf("%s/openresty/%s/conf/conf.d", constant.AppInstallDir, nginxInfo.Name) - if err := fileOp.CopyFile(fmt.Sprintf("%s/%s.conf", tmpPath, website.Alias), nginxConfPath); err != nil { - global.LOG.Errorf("handle recover from conf.d failed, err: %v", err) - return err - } - - switch website.Type { - case constant.Deployment: - app, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) - if err != nil { - return err - } - if err := handleAppRecover(&app, fmt.Sprintf("%s/%s.app.tar.gz", tmpPath, website.Alias), true, ""); err != nil { - global.LOG.Errorf("handle recover from app.tar.gz failed, err: %v", err) - return err - } - if _, err := compose.Restart(fmt.Sprintf("%s/%s/%s/docker-compose.yml", constant.AppInstallDir, app.App.Key, app.Name)); err != nil { - global.LOG.Errorf("docker-compose restart failed, err: %v", err) - return err - } - case constant.Runtime: - runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(website.RuntimeID)) - if err != nil { - return err - } - if runtime.Type == constant.RuntimeNode || runtime.Type == constant.RuntimeJava || runtime.Type == constant.RuntimeGo { - if err := handleRuntimeRecover(runtime, fmt.Sprintf("%s/%s.runtime.tar.gz", tmpPath, website.Alias), true, ""); err != nil { - return err - } - global.LOG.Info("put runtime.tar.gz into tmp dir successful") - } - } - - siteDir := fmt.Sprintf("%s/openresty/%s/www/sites", constant.AppInstallDir, nginxInfo.Name) - if err := handleUnTar(fmt.Sprintf("%s/%s.web.tar.gz", tmpPath, website.Alias), siteDir, ""); err != nil { - global.LOG.Errorf("handle recover from web.tar.gz failed, err: %v", err) - return err - } - stdout, err := cmd.Execf("docker exec -i %s nginx -s reload", nginxInfo.ContainerName) - if err != nil { - global.LOG.Errorf("nginx -s reload failed, err: %s", stdout) - return errors.New(string(stdout)) - } - - oldWebsite.ID = website.ID - if err := websiteRepo.SaveWithoutCtx(&oldWebsite); err != nil { - global.LOG.Errorf("handle save website data failed, err: %v", err) - return err - } - isOk = true - return nil -} - -func handleWebsiteBackup(website *model.Website, backupDir, fileName string, excludes string, secret string) error { - fileOp := files.NewFileOp() - tmpDir := fmt.Sprintf("%s/%s", backupDir, strings.ReplaceAll(fileName, ".tar.gz", "")) - if !fileOp.Stat(tmpDir) { - if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil { - return fmt.Errorf("mkdir %s failed, err: %v", backupDir, err) - } - } - defer func() { - _ = os.RemoveAll(tmpDir) - }() - - remarkInfo, _ := json.Marshal(website) - if err := fileOp.SaveFile(tmpDir+"/website.json", string(remarkInfo), fs.ModePerm); err != nil { - return err - } - global.LOG.Info("put website.json into tmp dir successful") - - nginxInfo, err := appInstallRepo.LoadBaseInfo(constant.AppOpenresty, "") - if err != nil { - return err - } - nginxConfFile := fmt.Sprintf("%s/openresty/%s/conf/conf.d/%s.conf", constant.AppInstallDir, nginxInfo.Name, website.Alias) - if err := fileOp.CopyFile(nginxConfFile, tmpDir); err != nil { - return err - } - global.LOG.Info("put openresty conf into tmp dir successful") - - switch website.Type { - case constant.Deployment: - app, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) - if err != nil { - return err - } - if err := handleAppBackup(&app, tmpDir, fmt.Sprintf("%s.app.tar.gz", website.Alias), excludes, ""); err != nil { - return err - } - global.LOG.Info("put app.tar.gz into tmp dir successful") - case constant.Runtime: - runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(website.RuntimeID)) - if err != nil { - return err - } - if runtime.Type == constant.RuntimeNode || runtime.Type == constant.RuntimeJava || runtime.Type == constant.RuntimeGo { - if err := handleRuntimeBackup(runtime, tmpDir, fmt.Sprintf("%s.runtime.tar.gz", website.Alias), excludes, ""); err != nil { - return err - } - global.LOG.Info("put runtime.tar.gz into tmp dir successful") - } - } - - websiteDir := fmt.Sprintf("%s/openresty/%s/www/sites/%s", constant.AppInstallDir, nginxInfo.Name, website.Alias) - if err := handleTar(websiteDir, tmpDir, fmt.Sprintf("%s.web.tar.gz", website.Alias), excludes, ""); err != nil { - return err - } - global.LOG.Info("put web.tar.gz into tmp dir successful, now start to tar tmp dir") - if err := handleTar(tmpDir, backupDir, fileName, "", secret); err != nil { - return err - } - - return nil -} - -func checkValidOfWebsite(oldWebsite, website *model.Website) error { - if oldWebsite.Alias != website.Alias || oldWebsite.Type != website.Type { - return buserr.WithDetail(constant.ErrBackupMatch, fmt.Sprintf("oldName: %s, oldType: %v", oldWebsite.Alias, oldWebsite.Type), nil) - } - if oldWebsite.AppInstallID != 0 { - _, err := appInstallRepo.GetFirst(commonRepo.WithByID(oldWebsite.AppInstallID)) - if err != nil { - return buserr.WithDetail(constant.ErrBackupMatch, "app", nil) - } - } - if oldWebsite.RuntimeID != 0 { - if _, err := runtimeRepo.GetFirst(commonRepo.WithByID(oldWebsite.RuntimeID)); err != nil { - return buserr.WithDetail(constant.ErrBackupMatch, "runtime", nil) - } - } - if oldWebsite.WebsiteSSLID != 0 { - if _, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(oldWebsite.WebsiteSSLID)); err != nil { - return buserr.WithDetail(constant.ErrBackupMatch, "ssl", nil) - } - } - return nil -} diff --git a/backend/app/service/clam.go b/backend/app/service/clam.go deleted file mode 100644 index 75c3371e4..000000000 --- a/backend/app/service/clam.go +++ /dev/null @@ -1,562 +0,0 @@ -package service - -import ( - "bufio" - "fmt" - "os" - "os/exec" - "path" - "path/filepath" - "sort" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/1Panel-dev/1Panel/backend/utils/systemctl" - "github.com/1Panel-dev/1Panel/backend/utils/xpack" - "github.com/jinzhu/copier" - "github.com/robfig/cron/v3" - - "github.com/pkg/errors" -) - -const ( - clamServiceNameCentOs = "clamd@scan.service" - clamServiceNameUbuntu = "clamav-daemon.service" - freshClamService = "clamav-freshclam.service" - resultDir = "clamav" -) - -type ClamService struct { - serviceName string -} - -type IClamService interface { - LoadBaseInfo() (dto.ClamBaseInfo, error) - Operate(operate string) error - SearchWithPage(search dto.SearchClamWithPage) (int64, interface{}, error) - Create(req dto.ClamCreate) error - Update(req dto.ClamUpdate) error - UpdateStatus(id uint, status string) error - Delete(req dto.ClamDelete) error - HandleOnce(req dto.OperateByID) error - LoadFile(req dto.ClamFileReq) (string, error) - UpdateFile(req dto.UpdateByNameAndFile) error - LoadRecords(req dto.ClamLogSearch) (int64, interface{}, error) - CleanRecord(req dto.OperateByID) error - - LoadRecordLog(req dto.ClamLogReq) (string, error) -} - -func NewIClamService() IClamService { - return &ClamService{} -} - -func (c *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) { - var baseInfo dto.ClamBaseInfo - baseInfo.Version = "-" - baseInfo.FreshVersion = "-" - exist1, _ := systemctl.IsExist(clamServiceNameCentOs) - if exist1 { - c.serviceName = clamServiceNameCentOs - baseInfo.IsExist = true - baseInfo.IsActive, _ = systemctl.IsActive(clamServiceNameCentOs) - } - exist2, _ := systemctl.IsExist(clamServiceNameUbuntu) - if exist2 { - c.serviceName = clamServiceNameUbuntu - baseInfo.IsExist = true - baseInfo.IsActive, _ = systemctl.IsActive(clamServiceNameUbuntu) - } - freshExist, _ := systemctl.IsExist(freshClamService) - if freshExist { - baseInfo.FreshIsExist = true - baseInfo.FreshIsActive, _ = systemctl.IsActive(freshClamService) - } - if !cmd.Which("clamdscan") { - baseInfo.IsActive = false - } - - if baseInfo.IsActive { - version, err := cmd.Exec("clamdscan --version") - if err == nil { - if strings.Contains(version, "/") { - baseInfo.Version = strings.TrimPrefix(strings.Split(version, "/")[0], "ClamAV ") - } else { - baseInfo.Version = strings.TrimPrefix(version, "ClamAV ") - } - } - } - if baseInfo.FreshIsActive { - version, err := cmd.Exec("freshclam --version") - if err == nil { - if strings.Contains(version, "/") { - baseInfo.FreshVersion = strings.TrimPrefix(strings.Split(version, "/")[0], "ClamAV ") - } else { - baseInfo.FreshVersion = strings.TrimPrefix(version, "ClamAV ") - } - } - } - return baseInfo, nil -} - -func (c *ClamService) Operate(operate string) error { - switch operate { - case "start", "restart", "stop": - stdout, err := cmd.Execf("systemctl %s %s", operate, c.serviceName) - if err != nil { - return fmt.Errorf("%s the %s failed, err: %s", operate, c.serviceName, stdout) - } - return nil - case "fresh-start", "fresh-restart", "fresh-stop": - stdout, err := cmd.Execf("systemctl %s %s", strings.TrimPrefix(operate, "fresh-"), freshClamService) - if err != nil { - return fmt.Errorf("%s the %s failed, err: %s", operate, c.serviceName, stdout) - } - return nil - default: - return fmt.Errorf("not support such operation: %v", operate) - } -} - -func (c *ClamService) SearchWithPage(req dto.SearchClamWithPage) (int64, interface{}, error) { - total, commands, err := clamRepo.Page(req.Page, req.PageSize, commonRepo.WithLikeName(req.Info), commonRepo.WithOrderRuleBy(req.OrderBy, req.Order)) - if err != nil { - return 0, nil, err - } - var datas []dto.ClamInfo - for _, command := range commands { - var item dto.ClamInfo - if err := copier.Copy(&item, &command); err != nil { - return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - item.LastHandleDate = "-" - datas = append(datas, item) - } - nyc, _ := time.LoadLocation(common.LoadTimeZone()) - for i := 0; i < len(datas); i++ { - logPaths := loadFileByName(datas[i].Name) - sort.Slice(logPaths, func(i, j int) bool { - return logPaths[i] > logPaths[j] - }) - if len(logPaths) != 0 { - t1, err := time.ParseInLocation(constant.DateTimeSlimLayout, logPaths[0], nyc) - if err != nil { - continue - } - datas[i].LastHandleDate = t1.Format(constant.DateTimeLayout) - } - } - return total, datas, err -} - -func (c *ClamService) Create(req dto.ClamCreate) error { - clam, _ := clamRepo.Get(commonRepo.WithByName(req.Name)) - if clam.ID != 0 { - return constant.ErrRecordExist - } - if err := copier.Copy(&clam, &req); err != nil { - return errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - if clam.InfectedStrategy == "none" || clam.InfectedStrategy == "remove" { - clam.InfectedDir = "" - } - if len(req.Spec) != 0 { - entryID, err := xpack.StartClam(clam, false) - if err != nil { - return err - } - clam.EntryID = entryID - clam.Status = constant.StatusEnable - } - if err := clamRepo.Create(&clam); err != nil { - return err - } - return nil -} - -func (c *ClamService) Update(req dto.ClamUpdate) error { - clam, _ := clamRepo.Get(commonRepo.WithByName(req.Name)) - if clam.ID == 0 { - return constant.ErrRecordNotFound - } - if req.InfectedStrategy == "none" || req.InfectedStrategy == "remove" { - req.InfectedDir = "" - } - var clamItem model.Clam - if err := copier.Copy(&clamItem, &req); err != nil { - return errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - clamItem.EntryID = clam.EntryID - upMap := map[string]interface{}{} - if len(clam.Spec) != 0 && clam.EntryID != 0 { - global.Cron.Remove(cron.EntryID(clamItem.EntryID)) - upMap["entry_id"] = 0 - } - if len(req.Spec) == 0 { - upMap["status"] = "" - upMap["entry_id"] = 0 - } - if len(req.Spec) != 0 && clam.Status != constant.StatusDisable { - newEntryID, err := xpack.StartClam(clamItem, true) - if err != nil { - return err - } - upMap["entry_id"] = newEntryID - } - if len(clam.Spec) == 0 && len(req.Spec) != 0 { - upMap["status"] = constant.StatusEnable - } - - upMap["name"] = req.Name - upMap["path"] = req.Path - upMap["infected_dir"] = req.InfectedDir - upMap["infected_strategy"] = req.InfectedStrategy - upMap["spec"] = req.Spec - upMap["description"] = req.Description - if err := clamRepo.Update(req.ID, upMap); err != nil { - return err - } - return nil -} - -func (c *ClamService) UpdateStatus(id uint, status string) error { - clam, _ := clamRepo.Get(commonRepo.WithByID(id)) - if clam.ID == 0 { - return constant.ErrRecordNotFound - } - var ( - entryID int - err error - ) - if status == constant.StatusEnable { - entryID, err = xpack.StartClam(clam, true) - if err != nil { - return err - } - } else { - global.Cron.Remove(cron.EntryID(clam.EntryID)) - global.LOG.Infof("stop cronjob entryID: %v", clam.EntryID) - } - - return clamRepo.Update(clam.ID, map[string]interface{}{"status": status, "entry_id": entryID}) -} - -func (c *ClamService) Delete(req dto.ClamDelete) error { - for _, id := range req.Ids { - clam, _ := clamRepo.Get(commonRepo.WithByID(id)) - if clam.ID == 0 { - continue - } - if req.RemoveRecord { - _ = os.RemoveAll(path.Join(global.CONF.System.DataDir, resultDir, clam.Name)) - } - if req.RemoveInfected { - _ = os.RemoveAll(path.Join(clam.InfectedDir, "1panel-infected", clam.Name)) - } - if err := clamRepo.Delete(commonRepo.WithByID(id)); err != nil { - return err - } - } - return nil -} - -func (c *ClamService) HandleOnce(req dto.OperateByID) error { - if !cmd.Which("clamdscan") { - return buserr.New("ErrClamdscanNotFound") - } - clam, _ := clamRepo.Get(commonRepo.WithByID(req.ID)) - if clam.ID == 0 { - return constant.ErrRecordNotFound - } - if cmd.CheckIllegal(clam.Path) { - return buserr.New(constant.ErrCmdIllegal) - } - timeNow := time.Now().Format(constant.DateTimeSlimLayout) - logFile := path.Join(global.CONF.System.DataDir, resultDir, clam.Name, timeNow) - if _, err := os.Stat(path.Dir(logFile)); err != nil { - _ = os.MkdirAll(path.Dir(logFile), os.ModePerm) - } - go func() { - strategy := "" - switch clam.InfectedStrategy { - case "remove": - strategy = "--remove" - case "move": - dir := path.Join(clam.InfectedDir, "1panel-infected", clam.Name, timeNow) - strategy = "--move=" + dir - if _, err := os.Stat(dir); err != nil { - _ = os.MkdirAll(dir, os.ModePerm) - } - case "copy": - dir := path.Join(clam.InfectedDir, "1panel-infected", clam.Name, timeNow) - strategy = "--copy=" + dir - if _, err := os.Stat(dir); err != nil { - _ = os.MkdirAll(dir, os.ModePerm) - } - } - global.LOG.Debugf("clamdscan --fdpass %s %s -l %s", strategy, clam.Path, logFile) - stdout, err := cmd.Execf("clamdscan --fdpass %s %s -l %s", strategy, clam.Path, logFile) - if err != nil { - global.LOG.Errorf("clamdscan failed, stdout: %v, err: %v", stdout, err) - } - }() - return nil -} - -func (c *ClamService) LoadRecords(req dto.ClamLogSearch) (int64, interface{}, error) { - clam, _ := clamRepo.Get(commonRepo.WithByID(req.ClamID)) - if clam.ID == 0 { - return 0, nil, constant.ErrRecordNotFound - } - logPaths := loadFileByName(clam.Name) - if len(logPaths) == 0 { - return 0, nil, nil - } - - var filterFiles []string - nyc, _ := time.LoadLocation(common.LoadTimeZone()) - for _, item := range logPaths { - t1, err := time.ParseInLocation(constant.DateTimeSlimLayout, item, nyc) - if err != nil { - continue - } - if t1.After(req.StartTime) && t1.Before(req.EndTime) { - filterFiles = append(filterFiles, item) - } - } - if len(filterFiles) == 0 { - return 0, nil, nil - } - - sort.Slice(filterFiles, func(i, j int) bool { - return filterFiles[i] > filterFiles[j] - }) - - var records []string - total, start, end := len(filterFiles), (req.Page-1)*req.PageSize, req.Page*req.PageSize - if start > total { - records = make([]string, 0) - } else { - if end >= total { - end = total - } - records = filterFiles[start:end] - } - - var datas []dto.ClamLog - for i := 0; i < len(records); i++ { - item := loadResultFromLog(path.Join(global.CONF.System.DataDir, resultDir, clam.Name, records[i])) - datas = append(datas, item) - } - return int64(total), datas, nil -} -func (c *ClamService) LoadRecordLog(req dto.ClamLogReq) (string, error) { - logPath := path.Join(global.CONF.System.DataDir, resultDir, req.ClamName, req.RecordName) - var tail string - if req.Tail != "0" { - tail = req.Tail - } else { - tail = "+1" - } - cmd := exec.Command("tail", "-n", tail, logPath) - stdout, err := cmd.CombinedOutput() - if err != nil { - return "", fmt.Errorf("tail -n %v failed, err: %v", req.Tail, err) - } - return string(stdout), nil -} - -func (c *ClamService) CleanRecord(req dto.OperateByID) error { - clam, _ := clamRepo.Get(commonRepo.WithByID(req.ID)) - if clam.ID == 0 { - return constant.ErrRecordNotFound - } - pathItem := path.Join(global.CONF.System.DataDir, resultDir, clam.Name) - _ = os.RemoveAll(pathItem) - return nil -} - -func (c *ClamService) LoadFile(req dto.ClamFileReq) (string, error) { - filePath := "" - switch req.Name { - case "clamd": - if c.serviceName == clamServiceNameUbuntu { - filePath = "/etc/clamav/clamd.conf" - } else { - filePath = "/etc/clamd.d/scan.conf" - } - case "clamd-log": - filePath = c.loadLogPath("clamd-log") - if len(filePath) != 0 { - break - } - if c.serviceName == clamServiceNameUbuntu { - filePath = "/var/log/clamav/clamav.log" - } else { - filePath = "/var/log/clamd.scan" - } - case "freshclam": - if c.serviceName == clamServiceNameUbuntu { - filePath = "/etc/clamav/freshclam.conf" - } else { - filePath = "/etc/freshclam.conf" - } - case "freshclam-log": - filePath = c.loadLogPath("freshclam-log") - if len(filePath) != 0 { - break - } - if c.serviceName == clamServiceNameUbuntu { - filePath = "/var/log/clamav/freshclam.log" - } else { - filePath = "/var/log/freshclam.log" - } - default: - return "", fmt.Errorf("not support such type") - } - if _, err := os.Stat(filePath); err != nil { - return "", buserr.New("ErrHttpReqNotFound") - } - var tail string - if req.Tail != "0" { - tail = req.Tail - } else { - tail = "+1" - } - cmd := exec.Command("tail", "-n", tail, filePath) - stdout, err := cmd.CombinedOutput() - if err != nil { - return "", fmt.Errorf("tail -n %v failed, err: %v", req.Tail, err) - } - return string(stdout), nil -} - -func (c *ClamService) UpdateFile(req dto.UpdateByNameAndFile) error { - filePath := "" - service := "" - switch req.Name { - case "clamd": - if c.serviceName == clamServiceNameUbuntu { - service = clamServiceNameUbuntu - filePath = "/etc/clamav/clamd.conf" - } else { - service = clamServiceNameCentOs - filePath = "/etc/clamd.d/scan.conf" - } - case "freshclam": - if c.serviceName == clamServiceNameUbuntu { - filePath = "/etc/clamav/freshclam.conf" - } else { - filePath = "/etc/freshclam.conf" - } - service = "clamav-freshclam.service" - default: - return fmt.Errorf("not support such type") - } - file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC, 0640) - if err != nil { - return err - } - defer file.Close() - write := bufio.NewWriter(file) - _, _ = write.WriteString(req.File) - write.Flush() - - _ = systemctl.Restart(service) - return nil -} - -func loadFileByName(name string) []string { - var logPaths []string - pathItem := path.Join(global.CONF.System.DataDir, resultDir, name) - _ = filepath.Walk(pathItem, func(path string, info os.FileInfo, err error) error { - if err != nil { - return nil - } - if info.IsDir() || info.Name() == name { - return nil - } - logPaths = append(logPaths, info.Name()) - return nil - }) - return logPaths -} -func loadResultFromLog(pathItem string) dto.ClamLog { - var data dto.ClamLog - data.Name = path.Base(pathItem) - data.Status = constant.StatusWaiting - file, err := os.ReadFile(pathItem) - if err != nil { - return data - } - lines := strings.Split(string(file), "\n") - for _, line := range lines { - if strings.Contains(line, "- SCAN SUMMARY -") { - data.Status = constant.StatusDone - } - if data.Status != constant.StatusDone { - continue - } - switch { - case strings.HasPrefix(line, "Infected files:"): - data.InfectedFiles = strings.TrimPrefix(line, "Infected files:") - case strings.HasPrefix(line, "Total errors:"): - data.TotalError = strings.TrimPrefix(line, "Total errors:") - case strings.HasPrefix(line, "Time:"): - if strings.Contains(line, "(") { - data.ScanTime = strings.ReplaceAll(strings.Split(line, "(")[1], ")", "") - continue - } - data.ScanTime = strings.TrimPrefix(line, "Time:") - case strings.HasPrefix(line, "Start Date:"): - data.ScanDate = strings.TrimPrefix(line, "Start Date:") - } - } - return data -} -func (c *ClamService) loadLogPath(name string) string { - confPath := "" - if name == "clamd-log" { - if c.serviceName == clamServiceNameUbuntu { - confPath = "/etc/clamav/clamd.conf" - } else { - confPath = "/etc/clamd.d/scan.conf" - } - } else { - if c.serviceName == clamServiceNameUbuntu { - confPath = "/etc/clamav/freshclam.conf" - } else { - confPath = "/etc/freshclam.conf" - } - } - if _, err := os.Stat(confPath); err != nil { - return "" - } - content, err := os.ReadFile(confPath) - if err != nil { - return "" - } - lines := strings.Split(string(content), "\n") - if name == "clamd-log" { - for _, line := range lines { - if strings.HasPrefix(line, "LogFile ") { - return strings.Trim(strings.ReplaceAll(line, "LogFile ", ""), " ") - } - } - } else { - for _, line := range lines { - if strings.HasPrefix(line, "UpdateLogFile ") { - return strings.Trim(strings.ReplaceAll(line, "UpdateLogFile ", ""), " ") - } - } - } - - return "" -} diff --git a/backend/app/service/command.go b/backend/app/service/command.go deleted file mode 100644 index d32e8f2c8..000000000 --- a/backend/app/service/command.go +++ /dev/null @@ -1,183 +0,0 @@ -package service - -import ( - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/jinzhu/copier" - "github.com/pkg/errors" -) - -type CommandService struct{} - -type ICommandService interface { - List() ([]dto.CommandInfo, error) - SearchForTree() ([]dto.CommandTree, error) - SearchWithPage(search dto.SearchCommandWithPage) (int64, interface{}, error) - Create(commandDto dto.CommandOperate) error - Update(id uint, upMap map[string]interface{}) error - Delete(ids []uint) error - - SearchRedisCommandWithPage(search dto.SearchWithPage) (int64, interface{}, error) - ListRedisCommand() ([]dto.RedisCommand, error) - SaveRedisCommand(commandDto dto.RedisCommand) error - DeleteRedisCommand(ids []uint) error -} - -func NewICommandService() ICommandService { - return &CommandService{} -} - -func (u *CommandService) List() ([]dto.CommandInfo, error) { - commands, err := commandRepo.GetList(commonRepo.WithOrderBy("name")) - if err != nil { - return nil, constant.ErrRecordNotFound - } - var dtoCommands []dto.CommandInfo - for _, command := range commands { - var item dto.CommandInfo - if err := copier.Copy(&item, &command); err != nil { - return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - dtoCommands = append(dtoCommands, item) - } - return dtoCommands, err -} - -func (u *CommandService) SearchForTree() ([]dto.CommandTree, error) { - cmdList, err := commandRepo.GetList(commonRepo.WithOrderBy("name")) - if err != nil { - return nil, err - } - groups, err := groupRepo.GetList(commonRepo.WithByType("command")) - if err != nil { - return nil, err - } - var lists []dto.CommandTree - for _, group := range groups { - var data dto.CommandTree - data.ID = group.ID + 10000 - data.Label = group.Name - for _, cmd := range cmdList { - if cmd.GroupID == group.ID { - data.Children = append(data.Children, dto.CommandInfo{ID: cmd.ID, Name: cmd.Name, Command: cmd.Command}) - } - } - if len(data.Children) != 0 { - lists = append(lists, data) - } - } - return lists, err -} - -func (u *CommandService) SearchWithPage(search dto.SearchCommandWithPage) (int64, interface{}, error) { - total, commands, err := commandRepo.Page(search.Page, search.PageSize, commandRepo.WithLikeName(search.Name), commonRepo.WithLikeName(search.Info), commonRepo.WithByGroupID(search.GroupID), commonRepo.WithOrderRuleBy(search.OrderBy, search.Order)) - if err != nil { - return 0, nil, err - } - groups, _ := groupRepo.GetList(commonRepo.WithByType("command"), commonRepo.WithOrderBy("name")) - var dtoCommands []dto.CommandInfo - for _, command := range commands { - var item dto.CommandInfo - if err := copier.Copy(&item, &command); err != nil { - return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - for _, group := range groups { - if command.GroupID == group.ID { - item.GroupBelong = group.Name - item.GroupID = group.ID - } - } - dtoCommands = append(dtoCommands, item) - } - return total, dtoCommands, err -} - -func (u *CommandService) Create(commandDto dto.CommandOperate) error { - command, _ := commandRepo.Get(commonRepo.WithByName(commandDto.Name)) - if command.ID != 0 { - return constant.ErrRecordExist - } - if err := copier.Copy(&command, &commandDto); err != nil { - return errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - if err := commandRepo.Create(&command); err != nil { - return err - } - return nil -} - -func (u *CommandService) Delete(ids []uint) error { - if len(ids) == 1 { - command, _ := commandRepo.Get(commonRepo.WithByID(ids[0])) - if command.ID == 0 { - return constant.ErrRecordNotFound - } - return commandRepo.Delete(commonRepo.WithByID(ids[0])) - } - return commandRepo.Delete(commonRepo.WithIdsIn(ids)) -} - -func (u *CommandService) Update(id uint, upMap map[string]interface{}) error { - return commandRepo.Update(id, upMap) -} - -func (u *CommandService) SearchRedisCommandWithPage(search dto.SearchWithPage) (int64, interface{}, error) { - total, commands, err := commandRepo.PageRedis(search.Page, search.PageSize, commandRepo.WithLikeName(search.Info)) - if err != nil { - return 0, nil, err - } - var dtoCommands []dto.RedisCommand - for _, command := range commands { - var item dto.RedisCommand - if err := copier.Copy(&item, &command); err != nil { - return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - dtoCommands = append(dtoCommands, item) - } - return total, dtoCommands, err -} - -func (u *CommandService) ListRedisCommand() ([]dto.RedisCommand, error) { - commands, err := commandRepo.GetRedisList() - if err != nil { - return nil, constant.ErrRecordNotFound - } - var dtoCommands []dto.RedisCommand - for _, command := range commands { - var item dto.RedisCommand - if err := copier.Copy(&item, &command); err != nil { - return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - dtoCommands = append(dtoCommands, item) - } - return dtoCommands, err -} - -func (u *CommandService) SaveRedisCommand(req dto.RedisCommand) error { - if req.ID == 0 { - command, _ := commandRepo.GetRedis(commonRepo.WithByName(req.Name)) - if command.ID != 0 { - return constant.ErrRecordExist - } - } - var command model.RedisCommand - if err := copier.Copy(&command, &req); err != nil { - return errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - if err := commandRepo.SaveRedis(&command); err != nil { - return err - } - return nil -} - -func (u *CommandService) DeleteRedisCommand(ids []uint) error { - if len(ids) == 1 { - command, _ := commandRepo.GetRedis(commonRepo.WithByID(ids[0])) - if command.ID == 0 { - return constant.ErrRecordNotFound - } - return commandRepo.DeleteRedis(commonRepo.WithByID(ids[0])) - } - return commandRepo.DeleteRedis(commonRepo.WithIdsIn(ids)) -} diff --git a/backend/app/service/compose_template.go b/backend/app/service/compose_template.go deleted file mode 100644 index e3bcb7a65..000000000 --- a/backend/app/service/compose_template.go +++ /dev/null @@ -1,80 +0,0 @@ -package service - -import ( - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/jinzhu/copier" - "github.com/pkg/errors" -) - -type ComposeTemplateService struct{} - -type IComposeTemplateService interface { - List() ([]dto.ComposeTemplateInfo, error) - SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error) - Create(composeDto dto.ComposeTemplateCreate) error - Update(id uint, upMap map[string]interface{}) error - Delete(ids []uint) error -} - -func NewIComposeTemplateService() IComposeTemplateService { - return &ComposeTemplateService{} -} - -func (u *ComposeTemplateService) List() ([]dto.ComposeTemplateInfo, error) { - composes, err := composeRepo.List() - if err != nil { - return nil, constant.ErrRecordNotFound - } - var dtoLists []dto.ComposeTemplateInfo - for _, compose := range composes { - var item dto.ComposeTemplateInfo - if err := copier.Copy(&item, &compose); err != nil { - return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - dtoLists = append(dtoLists, item) - } - return dtoLists, err -} - -func (u *ComposeTemplateService) SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error) { - total, composes, err := composeRepo.Page(req.Page, req.PageSize, commonRepo.WithLikeName(req.Info)) - var dtoComposeTemplates []dto.ComposeTemplateInfo - for _, compose := range composes { - var item dto.ComposeTemplateInfo - if err := copier.Copy(&item, &compose); err != nil { - return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - dtoComposeTemplates = append(dtoComposeTemplates, item) - } - return total, dtoComposeTemplates, err -} - -func (u *ComposeTemplateService) Create(composeDto dto.ComposeTemplateCreate) error { - compose, _ := composeRepo.Get(commonRepo.WithByName(composeDto.Name)) - if compose.ID != 0 { - return constant.ErrRecordExist - } - if err := copier.Copy(&compose, &composeDto); err != nil { - return errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - if err := composeRepo.Create(&compose); err != nil { - return err - } - return nil -} - -func (u *ComposeTemplateService) Delete(ids []uint) error { - if len(ids) == 1 { - compose, _ := composeRepo.Get(commonRepo.WithByID(ids[0])) - if compose.ID == 0 { - return constant.ErrRecordNotFound - } - return composeRepo.Delete(commonRepo.WithByID(ids[0])) - } - return composeRepo.Delete(commonRepo.WithIdsIn(ids)) -} - -func (u *ComposeTemplateService) Update(id uint, upMap map[string]interface{}) error { - return composeRepo.Update(id, upMap) -} diff --git a/backend/app/service/container.go b/backend/app/service/container.go deleted file mode 100644 index 08ea98e5d..000000000 --- a/backend/app/service/container.go +++ /dev/null @@ -1,1312 +0,0 @@ -package service - -import ( - "bufio" - "context" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "os" - "os/exec" - "path/filepath" - "sort" - "strconv" - "strings" - "sync" - "syscall" - "time" - "unicode/utf8" - - "github.com/gin-gonic/gin" - - "github.com/pkg/errors" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/1Panel-dev/1Panel/backend/utils/docker" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/mount" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/client" - "github.com/docker/go-connections/nat" - "github.com/gorilla/websocket" - v1 "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/shirou/gopsutil/v3/cpu" - "github.com/shirou/gopsutil/v3/mem" -) - -type ContainerService struct{} - -type IContainerService interface { - Page(req dto.PageContainer) (int64, interface{}, error) - List() ([]string, error) - PageNetwork(req dto.SearchWithPage) (int64, interface{}, error) - ListNetwork() ([]dto.Options, error) - PageVolume(req dto.SearchWithPage) (int64, interface{}, error) - ListVolume() ([]dto.Options, error) - PageCompose(req dto.SearchWithPage) (int64, interface{}, error) - CreateCompose(req dto.ComposeCreate) (string, error) - ComposeOperation(req dto.ComposeOperation) error - ContainerCreate(req dto.ContainerOperate) error - ContainerUpdate(req dto.ContainerOperate) error - ContainerUpgrade(req dto.ContainerUpgrade) error - ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error) - ContainerListStats() ([]dto.ContainerListStats, error) - LoadResourceLimit() (*dto.ResourceLimit, error) - ContainerRename(req dto.ContainerRename) error - ContainerCommit(req dto.ContainerCommit) error - ContainerLogClean(req dto.OperationWithName) error - ContainerOperation(req dto.ContainerOperation) error - ContainerLogs(wsConn *websocket.Conn, containerType, container, since, tail string, follow bool) error - DownloadContainerLogs(containerType, container, since, tail string, c *gin.Context) error - ContainerStats(id string) (*dto.ContainerStats, error) - Inspect(req dto.InspectReq) (string, error) - DeleteNetwork(req dto.BatchDelete) error - CreateNetwork(req dto.NetworkCreate) error - DeleteVolume(req dto.BatchDelete) error - CreateVolume(req dto.VolumeCreate) error - TestCompose(req dto.ComposeCreate) (bool, error) - ComposeUpdate(req dto.ComposeUpdate) error - Prune(req dto.ContainerPrune) (dto.ContainerPruneReport, error) - - LoadContainerLogs(req dto.OperationWithNameAndType) string -} - -func NewIContainerService() IContainerService { - return &ContainerService{} -} - -func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, error) { - var ( - records []types.Container - list []types.Container - ) - client, err := docker.NewDockerClient() - if err != nil { - return 0, nil, err - } - defer client.Close() - options := container.ListOptions{ - All: true, - } - if len(req.Filters) != 0 { - options.Filters = filters.NewArgs() - options.Filters.Add("label", req.Filters) - } - containers, err := client.ContainerList(context.Background(), options) - if err != nil { - return 0, nil, err - } - if req.ExcludeAppStore { - for _, item := range containers { - if created, ok := item.Labels[composeCreatedBy]; ok && created == "Apps" { - continue - } - list = append(list, item) - } - } else { - list = containers - } - - if len(req.Name) != 0 { - length, count := len(list), 0 - for count < length { - if !strings.Contains(list[count].Names[0][1:], req.Name) { - list = append(list[:count], list[(count+1):]...) - length-- - } else { - count++ - } - } - } - if req.State != "all" { - length, count := len(list), 0 - for count < length { - if list[count].State != req.State { - list = append(list[:count], list[(count+1):]...) - length-- - } else { - count++ - } - } - } - switch req.OrderBy { - case "name": - sort.Slice(list, func(i, j int) bool { - if req.Order == constant.OrderAsc { - return list[i].Names[0][1:] < list[j].Names[0][1:] - } - return list[i].Names[0][1:] > list[j].Names[0][1:] - }) - case "state": - sort.Slice(list, func(i, j int) bool { - if req.Order == constant.OrderAsc { - return list[i].State < list[j].State - } - return list[i].State > list[j].State - }) - default: - sort.Slice(list, func(i, j int) bool { - if req.Order == constant.OrderAsc { - return list[i].Created < list[j].Created - } - return list[i].Created > list[j].Created - }) - } - - total, start, end := len(list), (req.Page-1)*req.PageSize, req.Page*req.PageSize - if start > total { - records = make([]types.Container, 0) - } else { - if end >= total { - end = total - } - records = list[start:end] - } - - backDatas := make([]dto.ContainerInfo, len(records)) - for i := 0; i < len(records); i++ { - item := records[i] - IsFromCompose := false - if _, ok := item.Labels[composeProjectLabel]; ok { - IsFromCompose = true - } - IsFromApp := false - if created, ok := item.Labels[composeCreatedBy]; ok && created == "Apps" { - IsFromApp = true - } - - ports := loadContainerPort(item.Ports) - info := dto.ContainerInfo{ - ContainerID: item.ID, - CreateTime: time.Unix(item.Created, 0).Format(constant.DateTimeLayout), - Name: item.Names[0][1:], - ImageId: strings.Split(item.ImageID, ":")[1], - ImageName: item.Image, - State: item.State, - RunTime: item.Status, - Ports: ports, - IsFromApp: IsFromApp, - IsFromCompose: IsFromCompose, - } - install, _ := appInstallRepo.GetFirst(appInstallRepo.WithContainerName(info.Name)) - if install.ID > 0 { - info.AppInstallName = install.Name - info.AppName = install.App.Name - websites, _ := websiteRepo.GetBy(websiteRepo.WithAppInstallId(install.ID)) - for _, website := range websites { - info.Websites = append(info.Websites, website.PrimaryDomain) - } - } - backDatas[i] = info - if item.NetworkSettings != nil && len(item.NetworkSettings.Networks) > 0 { - networks := make([]string, 0, len(item.NetworkSettings.Networks)) - for key := range item.NetworkSettings.Networks { - networks = append(networks, item.NetworkSettings.Networks[key].IPAddress) - } - sort.Strings(networks) - backDatas[i].Network = networks - } - } - - return int64(total), backDatas, nil -} - -func (u *ContainerService) List() ([]string, error) { - client, err := docker.NewDockerClient() - if err != nil { - return nil, err - } - defer client.Close() - containers, err := client.ContainerList(context.Background(), container.ListOptions{All: true}) - if err != nil { - return nil, err - } - var datas []string - for _, container := range containers { - for _, name := range container.Names { - if len(name) != 0 { - datas = append(datas, strings.TrimPrefix(name, "/")) - } - } - } - - return datas, nil -} - -func (u *ContainerService) ContainerListStats() ([]dto.ContainerListStats, error) { - client, err := docker.NewDockerClient() - if err != nil { - return nil, err - } - defer client.Close() - list, err := client.ContainerList(context.Background(), container.ListOptions{All: true}) - if err != nil { - return nil, err - } - var datas []dto.ContainerListStats - var wg sync.WaitGroup - wg.Add(len(list)) - for i := 0; i < len(list); i++ { - go func(item types.Container) { - datas = append(datas, loadCpuAndMem(client, item.ID)) - wg.Done() - }(list[i]) - } - wg.Wait() - return datas, nil -} - -func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) { - client, err := docker.NewDockerClient() - if err != nil { - return "", err - } - defer client.Close() - var inspectInfo interface{} - switch req.Type { - case "container": - inspectInfo, err = client.ContainerInspect(context.Background(), req.ID) - case "image": - inspectInfo, _, err = client.ImageInspectWithRaw(context.Background(), req.ID) - case "network": - inspectInfo, err = client.NetworkInspect(context.TODO(), req.ID, types.NetworkInspectOptions{}) - case "volume": - inspectInfo, err = client.VolumeInspect(context.TODO(), req.ID) - } - if err != nil { - return "", err - } - bytes, err := json.Marshal(inspectInfo) - if err != nil { - return "", err - } - return string(bytes), nil -} - -func (u *ContainerService) Prune(req dto.ContainerPrune) (dto.ContainerPruneReport, error) { - report := dto.ContainerPruneReport{} - client, err := docker.NewDockerClient() - if err != nil { - return report, err - } - defer client.Close() - pruneFilters := filters.NewArgs() - if req.WithTagAll { - pruneFilters.Add("dangling", "false") - if req.PruneType != "image" { - pruneFilters.Add("until", "24h") - } - } - switch req.PruneType { - case "container": - rep, err := client.ContainersPrune(context.Background(), pruneFilters) - if err != nil { - return report, err - } - report.DeletedNumber = len(rep.ContainersDeleted) - report.SpaceReclaimed = int(rep.SpaceReclaimed) - case "image": - rep, err := client.ImagesPrune(context.Background(), pruneFilters) - if err != nil { - return report, err - } - report.DeletedNumber = len(rep.ImagesDeleted) - report.SpaceReclaimed = int(rep.SpaceReclaimed) - case "network": - rep, err := client.NetworksPrune(context.Background(), pruneFilters) - if err != nil { - return report, err - } - report.DeletedNumber = len(rep.NetworksDeleted) - case "volume": - versions, err := client.ServerVersion(context.Background()) - if err != nil { - return report, err - } - if common.ComparePanelVersion(versions.APIVersion, "1.42") { - pruneFilters.Add("all", "true") - } - rep, err := client.VolumesPrune(context.Background(), pruneFilters) - if err != nil { - return report, err - } - report.DeletedNumber = len(rep.VolumesDeleted) - report.SpaceReclaimed = int(rep.SpaceReclaimed) - case "buildcache": - opts := types.BuildCachePruneOptions{} - opts.All = true - rep, err := client.BuildCachePrune(context.Background(), opts) - if err != nil { - return report, err - } - report.DeletedNumber = len(rep.CachesDeleted) - report.SpaceReclaimed = int(rep.SpaceReclaimed) - } - return report, nil -} - -func (u *ContainerService) LoadResourceLimit() (*dto.ResourceLimit, error) { - cpuCounts, err := cpu.Counts(true) - if err != nil { - return nil, fmt.Errorf("load cpu limit failed, err: %v", err) - } - memoryInfo, err := mem.VirtualMemory() - if err != nil { - return nil, fmt.Errorf("load memory limit failed, err: %v", err) - } - - data := dto.ResourceLimit{ - CPU: cpuCounts, - Memory: memoryInfo.Total, - } - return &data, nil -} - -func (u *ContainerService) ContainerCreate(req dto.ContainerOperate) error { - client, err := docker.NewDockerClient() - if err != nil { - return err - } - defer client.Close() - ctx := context.Background() - newContainer, _ := client.ContainerInspect(ctx, req.Name) - if newContainer.ContainerJSONBase != nil { - return buserr.New(constant.ErrContainerName) - } - - if !checkImageExist(client, req.Image) || req.ForcePull { - if err := pullImages(ctx, client, req.Image); err != nil { - if !req.ForcePull { - return err - } - global.LOG.Errorf("force pull image %s failed, err: %v", req.Image, err) - } - } - imageInfo, _, err := client.ImageInspectWithRaw(ctx, req.Image) - if err != nil { - return err - } - if len(req.Entrypoint) == 0 { - req.Entrypoint = imageInfo.Config.Entrypoint - } - if len(req.Cmd) == 0 { - req.Cmd = imageInfo.Config.Cmd - } - config, hostConf, networkConf, err := loadConfigInfo(true, req, nil) - if err != nil { - return err - } - global.LOG.Infof("new container info %s has been made, now start to create", req.Name) - con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name) - if err != nil { - _ = client.ContainerRemove(ctx, req.Name, container.RemoveOptions{RemoveVolumes: true, Force: true}) - return err - } - global.LOG.Infof("create container %s successful! now check if the container is started and delete the container information if it is not.", req.Name) - if err := client.ContainerStart(ctx, con.ID, container.StartOptions{}); err != nil { - _ = client.ContainerRemove(ctx, req.Name, container.RemoveOptions{RemoveVolumes: true, Force: true}) - return fmt.Errorf("create successful but start failed, err: %v", err) - } - return nil -} - -func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error) { - client, err := docker.NewDockerClient() - if err != nil { - return nil, err - } - defer client.Close() - ctx := context.Background() - oldContainer, err := client.ContainerInspect(ctx, req.Name) - if err != nil { - return nil, err - } - - var data dto.ContainerOperate - data.ContainerID = oldContainer.ID - data.Name = strings.ReplaceAll(oldContainer.Name, "/", "") - data.Image = oldContainer.Config.Image - if oldContainer.NetworkSettings != nil { - for network := range oldContainer.NetworkSettings.Networks { - data.Network = network - break - } - } - - networkSettings := oldContainer.NetworkSettings - bridgeNetworkSettings := networkSettings.Networks[data.Network] - if bridgeNetworkSettings.IPAMConfig != nil { - ipv4Address := bridgeNetworkSettings.IPAMConfig.IPv4Address - data.Ipv4 = ipv4Address - ipv6Address := bridgeNetworkSettings.IPAMConfig.IPv6Address - data.Ipv6 = ipv6Address - } else { - data.Ipv4 = bridgeNetworkSettings.IPAddress - } - - data.Cmd = oldContainer.Config.Cmd - data.OpenStdin = oldContainer.Config.OpenStdin - data.Tty = oldContainer.Config.Tty - data.Entrypoint = oldContainer.Config.Entrypoint - data.Env = oldContainer.Config.Env - data.CPUShares = oldContainer.HostConfig.CPUShares - for key, val := range oldContainer.Config.Labels { - data.Labels = append(data.Labels, fmt.Sprintf("%s=%s", key, val)) - } - for key, val := range oldContainer.HostConfig.PortBindings { - var itemPort dto.PortHelper - if !strings.Contains(string(key), "/") { - continue - } - itemPort.ContainerPort = strings.Split(string(key), "/")[0] - itemPort.Protocol = strings.Split(string(key), "/")[1] - for _, binds := range val { - itemPort.HostIP = binds.HostIP - itemPort.HostPort = binds.HostPort - data.ExposedPorts = append(data.ExposedPorts, itemPort) - } - } - data.AutoRemove = oldContainer.HostConfig.AutoRemove - data.Privileged = oldContainer.HostConfig.Privileged - data.PublishAllPorts = oldContainer.HostConfig.PublishAllPorts - data.RestartPolicy = string(oldContainer.HostConfig.RestartPolicy.Name) - if oldContainer.HostConfig.NanoCPUs != 0 { - data.NanoCPUs = float64(oldContainer.HostConfig.NanoCPUs) / 1000000000 - } - if oldContainer.HostConfig.Memory != 0 { - data.Memory = float64(oldContainer.HostConfig.Memory) / 1024 / 1024 - } - data.Volumes = loadVolumeBinds(oldContainer.Mounts) - - return &data, nil -} - -func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error { - client, err := docker.NewDockerClient() - if err != nil { - return err - } - defer client.Close() - ctx := context.Background() - newContainer, _ := client.ContainerInspect(ctx, req.Name) - if newContainer.ContainerJSONBase != nil && newContainer.ID != req.ContainerID { - return buserr.New(constant.ErrContainerName) - } - - oldContainer, err := client.ContainerInspect(ctx, req.ContainerID) - if err != nil { - return err - } - if !checkImageExist(client, req.Image) || req.ForcePull { - if err := pullImages(ctx, client, req.Image); err != nil { - if !req.ForcePull { - return err - } - return fmt.Errorf("pull image %s failed, err: %v", req.Image, err) - } - } - - if err := client.ContainerRemove(ctx, req.ContainerID, container.RemoveOptions{Force: true}); err != nil { - return err - } - - config, hostConf, networkConf, err := loadConfigInfo(false, req, &oldContainer) - if err != nil { - reCreateAfterUpdate(req.Name, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings) - return err - } - - global.LOG.Infof("new container info %s has been update, now start to recreate", req.Name) - con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name) - if err != nil { - reCreateAfterUpdate(req.Name, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings) - return fmt.Errorf("update container failed, err: %v", err) - } - global.LOG.Infof("update container %s successful! now check if the container is started.", req.Name) - if err := client.ContainerStart(ctx, con.ID, container.StartOptions{}); err != nil { - return fmt.Errorf("update successful but start failed, err: %v", err) - } - - return nil -} - -func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error { - client, err := docker.NewDockerClient() - if err != nil { - return err - } - defer client.Close() - ctx := context.Background() - oldContainer, err := client.ContainerInspect(ctx, req.Name) - if err != nil { - return err - } - if !checkImageExist(client, req.Image) || req.ForcePull { - if err := pullImages(ctx, client, req.Image); err != nil { - if !req.ForcePull { - return err - } - return fmt.Errorf("pull image %s failed, err: %v", req.Image, err) - } - } - config := oldContainer.Config - config.Image = req.Image - hostConf := oldContainer.HostConfig - var networkConf network.NetworkingConfig - if oldContainer.NetworkSettings != nil { - for networkKey := range oldContainer.NetworkSettings.Networks { - networkConf.EndpointsConfig = map[string]*network.EndpointSettings{networkKey: {}} - break - } - } - if err := client.ContainerRemove(ctx, req.Name, container.RemoveOptions{Force: true}); err != nil { - return err - } - - global.LOG.Infof("new container info %s has been update, now start to recreate", req.Name) - con, err := client.ContainerCreate(ctx, config, hostConf, &networkConf, &v1.Platform{}, req.Name) - if err != nil { - reCreateAfterUpdate(req.Name, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings) - return fmt.Errorf("upgrade container failed, err: %v", err) - } - global.LOG.Infof("upgrade container %s successful! now check if the container is started.", req.Name) - if err := client.ContainerStart(ctx, con.ID, container.StartOptions{}); err != nil { - return fmt.Errorf("upgrade successful but start failed, err: %v", err) - } - - return nil -} - -func (u *ContainerService) ContainerRename(req dto.ContainerRename) error { - ctx := context.Background() - client, err := docker.NewDockerClient() - if err != nil { - return err - } - defer client.Close() - - newContainer, _ := client.ContainerInspect(ctx, req.NewName) - if newContainer.ContainerJSONBase != nil { - return buserr.New(constant.ErrContainerName) - } - return client.ContainerRename(ctx, req.Name, req.NewName) -} - -func (u *ContainerService) ContainerCommit(req dto.ContainerCommit) error { - ctx := context.Background() - client, err := docker.NewDockerClient() - if err != nil { - return err - } - defer client.Close() - options := container.CommitOptions{ - Reference: req.NewImageName, - Comment: req.Comment, - Author: req.Author, - Changes: nil, - Pause: req.Pause, - Config: nil, - } - _, err = client.ContainerCommit(ctx, req.ContainerId, options) - if err != nil { - return fmt.Errorf("failed to commit container, err: %v", err) - } - return nil -} - -func (u *ContainerService) ContainerOperation(req dto.ContainerOperation) error { - var err error - ctx := context.Background() - client, err := docker.NewDockerClient() - if err != nil { - return err - } - defer client.Close() - for _, item := range req.Names { - global.LOG.Infof("start container %s operation %s", item, req.Operation) - switch req.Operation { - case constant.ContainerOpStart: - err = client.ContainerStart(ctx, item, container.StartOptions{}) - case constant.ContainerOpStop: - err = client.ContainerStop(ctx, item, container.StopOptions{}) - case constant.ContainerOpRestart: - err = client.ContainerRestart(ctx, item, container.StopOptions{}) - case constant.ContainerOpKill: - err = client.ContainerKill(ctx, item, "SIGKILL") - case constant.ContainerOpPause: - err = client.ContainerPause(ctx, item) - case constant.ContainerOpUnpause: - err = client.ContainerUnpause(ctx, item) - case constant.ContainerOpRemove: - err = client.ContainerRemove(ctx, item, container.RemoveOptions{RemoveVolumes: true, Force: true}) - } - } - return err -} - -func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error { - client, err := docker.NewDockerClient() - if err != nil { - return err - } - defer client.Close() - ctx := context.Background() - containerItem, err := client.ContainerInspect(ctx, req.Name) - if err != nil { - return err - } - if err := client.ContainerStop(ctx, containerItem.ID, container.StopOptions{}); err != nil { - return err - } - file, err := os.OpenFile(containerItem.LogPath, os.O_RDWR|os.O_CREATE, 0666) - if err != nil { - return err - } - defer file.Close() - if err = file.Truncate(0); err != nil { - return err - } - _, _ = file.Seek(0, 0) - - files, _ := filepath.Glob(fmt.Sprintf("%s.*", containerItem.LogPath)) - for _, file := range files { - _ = os.Remove(file) - } - - if err := client.ContainerStart(ctx, containerItem.ID, container.StartOptions{}); err != nil { - return err - } - return nil -} - -func (u *ContainerService) ContainerLogs(wsConn *websocket.Conn, containerType, container, since, tail string, follow bool) error { - defer func() { wsConn.Close() }() - if cmd.CheckIllegal(container, since, tail) { - return buserr.New(constant.ErrCmdIllegal) - } - commandName := "docker" - commandArg := []string{"logs", container} - if containerType == "compose" { - commandName = "docker-compose" - commandArg = []string{"-f", container, "logs"} - } - if tail != "0" { - commandArg = append(commandArg, "--tail") - commandArg = append(commandArg, tail) - } - if since != "all" { - commandArg = append(commandArg, "--since") - commandArg = append(commandArg, since) - } - if follow { - commandArg = append(commandArg, "-f") - } - if !follow { - cmd := exec.Command(commandName, commandArg...) - cmd.Stderr = cmd.Stdout - stdout, _ := cmd.CombinedOutput() - if !utf8.Valid(stdout) { - return errors.New("invalid utf8") - } - if err := wsConn.WriteMessage(websocket.TextMessage, stdout); err != nil { - global.LOG.Errorf("send message with log to ws failed, err: %v", err) - } - return nil - } - - cmd := exec.Command(commandName, commandArg...) - stdout, err := cmd.StdoutPipe() - if err != nil { - _ = cmd.Process.Signal(syscall.SIGTERM) - return err - } - cmd.Stderr = cmd.Stdout - if err := cmd.Start(); err != nil { - _ = cmd.Process.Signal(syscall.SIGTERM) - return err - } - exitCh := make(chan struct{}) - go func() { - _, wsData, _ := wsConn.ReadMessage() - if string(wsData) == "close conn" { - _ = cmd.Process.Signal(syscall.SIGTERM) - exitCh <- struct{}{} - } - }() - - go func() { - buffer := make([]byte, 1024) - for { - select { - case <-exitCh: - return - default: - n, err := stdout.Read(buffer) - if err != nil { - if err == io.EOF { - return - } - global.LOG.Errorf("read bytes from log failed, err: %v", err) - return - } - if !utf8.Valid(buffer[:n]) { - continue - } - if err = wsConn.WriteMessage(websocket.TextMessage, buffer[:n]); err != nil { - global.LOG.Errorf("send message with log to ws failed, err: %v", err) - return - } - } - } - }() - _ = cmd.Wait() - return nil -} - -func (u *ContainerService) DownloadContainerLogs(containerType, container, since, tail string, c *gin.Context) error { - if cmd.CheckIllegal(container, since, tail) { - return buserr.New(constant.ErrCmdIllegal) - } - commandName := "docker" - commandArg := []string{"logs", container} - if containerType == "compose" { - commandName = "docker-compose" - commandArg = []string{"-f", container, "logs"} - } - if tail != "0" { - commandArg = append(commandArg, "--tail") - commandArg = append(commandArg, tail) - } - if since != "all" { - commandArg = append(commandArg, "--since") - commandArg = append(commandArg, since) - } - - cmd := exec.Command(commandName, commandArg...) - stdout, err := cmd.StdoutPipe() - if err != nil { - _ = cmd.Process.Signal(syscall.SIGTERM) - return err - } - cmd.Stderr = cmd.Stdout - if err := cmd.Start(); err != nil { - _ = cmd.Process.Signal(syscall.SIGTERM) - return err - } - - tempFile, err := os.CreateTemp("", "cmd_output_*.txt") - if err != nil { - return err - } - defer tempFile.Close() - defer func() { - if err := os.Remove(tempFile.Name()); err != nil { - global.LOG.Errorf("os.Remove() failed: %v", err) - } - }() - errCh := make(chan error) - go func() { - scanner := bufio.NewScanner(stdout) - for scanner.Scan() { - line := scanner.Text() - if _, err := tempFile.WriteString(line + "\n"); err != nil { - errCh <- err - return - } - } - if err := scanner.Err(); err != nil { - errCh <- err - return - } - errCh <- nil - }() - select { - case err := <-errCh: - if err != nil { - global.LOG.Errorf("Error: %v", err) - } - case <-time.After(3 * time.Second): - global.LOG.Errorf("Timeout reached") - } - info, _ := tempFile.Stat() - - c.Header("Content-Length", strconv.FormatInt(info.Size(), 10)) - c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(info.Name())) - http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), tempFile) - return nil -} - -func (u *ContainerService) ContainerStats(id string) (*dto.ContainerStats, error) { - client, err := docker.NewDockerClient() - if err != nil { - return nil, err - } - defer client.Close() - res, err := client.ContainerStats(context.TODO(), id, false) - if err != nil { - return nil, err - } - defer res.Body.Close() - - body, err := io.ReadAll(res.Body) - if err != nil { - res.Body.Close() - return nil, err - } - res.Body.Close() - var stats *types.StatsJSON - if err := json.Unmarshal(body, &stats); err != nil { - return nil, err - } - var data dto.ContainerStats - data.CPUPercent = calculateCPUPercentUnix(stats) - data.IORead, data.IOWrite = calculateBlockIO(stats.BlkioStats) - data.Memory = float64(stats.MemoryStats.Usage) / 1024 / 1024 - if cache, ok := stats.MemoryStats.Stats["cache"]; ok { - data.Cache = float64(cache) / 1024 / 1024 - } - data.NetworkRX, data.NetworkTX = calculateNetwork(stats.Networks) - data.ShotTime = stats.Read - return &data, nil -} - -func (u *ContainerService) LoadContainerLogs(req dto.OperationWithNameAndType) string { - filePath := "" - if req.Type == "compose-detail" { - cli, err := docker.NewDockerClient() - if err != nil { - return "" - } - defer cli.Close() - options := container.ListOptions{All: true} - options.Filters = filters.NewArgs() - options.Filters.Add("label", fmt.Sprintf("%s=%s", composeProjectLabel, req.Name)) - containers, err := cli.ContainerList(context.Background(), options) - if err != nil { - return "" - } - for _, container := range containers { - config := container.Labels[composeConfigLabel] - workdir := container.Labels[composeWorkdirLabel] - if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) { - filePath = config - break - } else { - filePath = workdir - break - } - } - } - if _, err := os.Stat(filePath); err != nil { - return "" - } - content, err := os.ReadFile(filePath) - if err != nil { - return "" - } - return string(content) -} - -func stringsToMap(list []string) map[string]string { - var labelMap = make(map[string]string) - for _, label := range list { - if strings.Contains(label, "=") { - sps := strings.SplitN(label, "=", 2) - labelMap[sps[0]] = sps[1] - } - } - return labelMap -} - -func calculateCPUPercentUnix(stats *types.StatsJSON) float64 { - cpuPercent := 0.0 - cpuDelta := float64(stats.CPUStats.CPUUsage.TotalUsage) - float64(stats.PreCPUStats.CPUUsage.TotalUsage) - systemDelta := float64(stats.CPUStats.SystemUsage) - float64(stats.PreCPUStats.SystemUsage) - - if systemDelta > 0.0 && cpuDelta > 0.0 { - cpuPercent = (cpuDelta / systemDelta) * 100.0 - if len(stats.CPUStats.CPUUsage.PercpuUsage) != 0 { - cpuPercent = cpuPercent * float64(len(stats.CPUStats.CPUUsage.PercpuUsage)) - } - } - return cpuPercent -} -func calculateMemPercentUnix(memStats types.MemoryStats) float64 { - memPercent := 0.0 - memUsage := float64(memStats.Usage) - memLimit := float64(memStats.Limit) - if memUsage > 0.0 && memLimit > 0.0 { - memPercent = (memUsage / memLimit) * 100.0 - } - return memPercent -} -func calculateBlockIO(blkio types.BlkioStats) (blkRead float64, blkWrite float64) { - for _, bioEntry := range blkio.IoServiceBytesRecursive { - switch strings.ToLower(bioEntry.Op) { - case "read": - blkRead = (blkRead + float64(bioEntry.Value)) / 1024 / 1024 - case "write": - blkWrite = (blkWrite + float64(bioEntry.Value)) / 1024 / 1024 - } - } - return -} -func calculateNetwork(network map[string]types.NetworkStats) (float64, float64) { - var rx, tx float64 - - for _, v := range network { - rx += float64(v.RxBytes) / 1024 - tx += float64(v.TxBytes) / 1024 - } - return rx, tx -} - -func checkImageExist(client *client.Client, imageItem string) bool { - images, err := client.ImageList(context.Background(), image.ListOptions{}) - if err != nil { - return false - } - - for _, img := range images { - for _, tag := range img.RepoTags { - if tag == imageItem || tag == imageItem+":latest" { - return true - } - } - } - return false -} - -func pullImages(ctx context.Context, client *client.Client, imageName string) error { - options := image.PullOptions{} - repos, _ := imageRepoRepo.List() - if len(repos) != 0 { - for _, repo := range repos { - if strings.HasPrefix(imageName, repo.DownloadUrl) && repo.Auth { - authConfig := registry.AuthConfig{ - Username: repo.Username, - Password: repo.Password, - } - encodedJSON, err := json.Marshal(authConfig) - if err != nil { - return err - } - authStr := base64.URLEncoding.EncodeToString(encodedJSON) - options.RegistryAuth = authStr - } - } - } else { - hasAuth, authStr := loadAuthInfo(imageName) - if hasAuth { - options.RegistryAuth = authStr - } - } - out, err := client.ImagePull(ctx, imageName, options) - if err != nil { - return err - } - defer out.Close() - _, err = io.Copy(io.Discard, out) - if err != nil { - return err - } - return nil -} - -func loadCpuAndMem(client *client.Client, container string) dto.ContainerListStats { - data := dto.ContainerListStats{ - ContainerID: container, - } - res, err := client.ContainerStats(context.Background(), container, false) - if err != nil { - return data - } - - defer res.Body.Close() - body, err := io.ReadAll(res.Body) - if err != nil { - return data - } - var stats *types.StatsJSON - if err := json.Unmarshal(body, &stats); err != nil { - return data - } - - data.CPUTotalUsage = stats.CPUStats.CPUUsage.TotalUsage - stats.PreCPUStats.CPUUsage.TotalUsage - data.SystemUsage = stats.CPUStats.SystemUsage - stats.PreCPUStats.SystemUsage - data.CPUPercent = calculateCPUPercentUnix(stats) - data.PercpuUsage = len(stats.CPUStats.CPUUsage.PercpuUsage) - - data.MemoryCache = stats.MemoryStats.Stats["cache"] - data.MemoryUsage = stats.MemoryStats.Usage - data.MemoryLimit = stats.MemoryStats.Limit - - data.MemoryPercent = calculateMemPercentUnix(stats.MemoryStats) - return data -} - -func checkPortStats(ports []dto.PortHelper) (nat.PortMap, error) { - portMap := make(nat.PortMap) - if len(ports) == 0 { - return portMap, nil - } - for _, port := range ports { - if strings.Contains(port.ContainerPort, "-") { - if !strings.Contains(port.HostPort, "-") { - return portMap, buserr.New(constant.ErrPortRules) - } - hostStart, _ := strconv.Atoi(strings.Split(port.HostPort, "-")[0]) - hostEnd, _ := strconv.Atoi(strings.Split(port.HostPort, "-")[1]) - containerStart, _ := strconv.Atoi(strings.Split(port.ContainerPort, "-")[0]) - containerEnd, _ := strconv.Atoi(strings.Split(port.ContainerPort, "-")[1]) - if (hostEnd-hostStart) <= 0 || (containerEnd-containerStart) <= 0 { - return portMap, buserr.New(constant.ErrPortRules) - } - if (containerEnd - containerStart) != (hostEnd - hostStart) { - return portMap, buserr.New(constant.ErrPortRules) - } - for i := 0; i <= hostEnd-hostStart; i++ { - bindItem := nat.PortBinding{HostPort: strconv.Itoa(hostStart + i), HostIP: port.HostIP} - portMap[nat.Port(fmt.Sprintf("%d/%s", containerStart+i, port.Protocol))] = []nat.PortBinding{bindItem} - } - for i := hostStart; i <= hostEnd; i++ { - if common.ScanPort(i) { - return portMap, buserr.WithDetail(constant.ErrPortInUsed, i, nil) - } - } - } else { - portItem := 0 - if strings.Contains(port.HostPort, "-") { - portItem, _ = strconv.Atoi(strings.Split(port.HostPort, "-")[0]) - } else { - portItem, _ = strconv.Atoi(port.HostPort) - } - if common.ScanPort(portItem) { - return portMap, buserr.WithDetail(constant.ErrPortInUsed, portItem, nil) - } - bindItem := nat.PortBinding{HostPort: strconv.Itoa(portItem), HostIP: port.HostIP} - portMap[nat.Port(fmt.Sprintf("%s/%s", port.ContainerPort, port.Protocol))] = []nat.PortBinding{bindItem} - } - } - return portMap, nil -} - -func loadConfigInfo(isCreate bool, req dto.ContainerOperate, oldContainer *types.ContainerJSON) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) { - var config container.Config - var hostConf container.HostConfig - if !isCreate { - config = *oldContainer.Config - hostConf = *oldContainer.HostConfig - } - var networkConf network.NetworkingConfig - - portMap, err := checkPortStats(req.ExposedPorts) - if err != nil { - return nil, nil, nil, err - } - exposed := make(nat.PortSet) - for port := range portMap { - exposed[port] = struct{}{} - } - config.Image = req.Image - config.Cmd = req.Cmd - config.Entrypoint = req.Entrypoint - config.Env = req.Env - config.Labels = stringsToMap(req.Labels) - config.ExposedPorts = exposed - config.OpenStdin = req.OpenStdin - config.Tty = req.Tty - - if len(req.Network) != 0 { - switch req.Network { - case "host", "none", "bridge": - hostConf.NetworkMode = container.NetworkMode(req.Network) - } - if req.Ipv4 != "" || req.Ipv6 != "" { - networkConf.EndpointsConfig = map[string]*network.EndpointSettings{ - req.Network: { - IPAMConfig: &network.EndpointIPAMConfig{ - IPv4Address: req.Ipv4, - IPv6Address: req.Ipv6, - }, - }} - } else { - networkConf.EndpointsConfig = map[string]*network.EndpointSettings{req.Network: {}} - } - } else { - if req.Ipv4 != "" || req.Ipv6 != "" { - return nil, nil, nil, fmt.Errorf("please set up the network") - } - networkConf = network.NetworkingConfig{} - } - - hostConf.Privileged = req.Privileged - hostConf.AutoRemove = req.AutoRemove - hostConf.CPUShares = req.CPUShares - hostConf.PublishAllPorts = req.PublishAllPorts - hostConf.RestartPolicy = container.RestartPolicy{Name: container.RestartPolicyMode(req.RestartPolicy)} - if req.RestartPolicy == "on-failure" { - hostConf.RestartPolicy.MaximumRetryCount = 5 - } - hostConf.NanoCPUs = int64(req.NanoCPUs * 1000000000) - hostConf.Memory = int64(req.Memory * 1024 * 1024) - hostConf.MemorySwap = 0 - hostConf.PortBindings = portMap - hostConf.Binds = []string{} - hostConf.Mounts = []mount.Mount{} - config.Volumes = make(map[string]struct{}) - for _, volume := range req.Volumes { - if volume.Type == "volume" { - hostConf.Mounts = append(hostConf.Mounts, mount.Mount{ - Type: mount.Type(volume.Type), - Source: volume.SourceDir, - Target: volume.ContainerDir, - }) - config.Volumes[volume.ContainerDir] = struct{}{} - } else { - hostConf.Binds = append(hostConf.Binds, fmt.Sprintf("%s:%s:%s", volume.SourceDir, volume.ContainerDir, volume.Mode)) - } - } - return &config, &hostConf, &networkConf, nil -} - -func reCreateAfterUpdate(name string, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkConf *types.NetworkSettings) { - ctx := context.Background() - - var oldNetworkConf network.NetworkingConfig - if networkConf != nil { - for networkKey := range networkConf.Networks { - oldNetworkConf.EndpointsConfig = map[string]*network.EndpointSettings{networkKey: {}} - break - } - } - - oldContainer, err := client.ContainerCreate(ctx, config, hostConf, &oldNetworkConf, &v1.Platform{}, name) - if err != nil { - global.LOG.Errorf("recreate after container update failed, err: %v", err) - return - } - if err := client.ContainerStart(ctx, oldContainer.ID, container.StartOptions{}); err != nil { - global.LOG.Errorf("restart after container update failed, err: %v", err) - } - global.LOG.Errorf("recreate after container update successful") -} - -func loadVolumeBinds(binds []types.MountPoint) []dto.VolumeHelper { - var datas []dto.VolumeHelper - for _, bind := range binds { - var volumeItem dto.VolumeHelper - volumeItem.Type = string(bind.Type) - if bind.Type == "volume" { - volumeItem.SourceDir = bind.Name - } else { - volumeItem.SourceDir = bind.Source - } - volumeItem.ContainerDir = bind.Destination - volumeItem.Mode = "ro" - if bind.RW { - volumeItem.Mode = "rw" - } - datas = append(datas, volumeItem) - } - return datas -} - -func loadContainerPort(ports []types.Port) []string { - var ( - ipv4Ports []types.Port - ipv6Ports []types.Port - ) - for _, port := range ports { - if strings.Contains(port.IP, ":") { - ipv6Ports = append(ipv6Ports, port) - } else { - ipv4Ports = append(ipv4Ports, port) - } - } - list1 := simplifyPort(ipv4Ports) - list2 := simplifyPort(ipv6Ports) - return append(list1, list2...) -} -func simplifyPort(ports []types.Port) []string { - var datas []string - if len(ports) == 0 { - return datas - } - if len(ports) == 1 { - ip := "" - if len(ports[0].IP) != 0 { - ip = ports[0].IP + ":" - } - itemPortStr := fmt.Sprintf("%s%v/%s", ip, ports[0].PrivatePort, ports[0].Type) - if ports[0].PublicPort != 0 { - itemPortStr = fmt.Sprintf("%s%v->%v/%s", ip, ports[0].PublicPort, ports[0].PrivatePort, ports[0].Type) - } - datas = append(datas, itemPortStr) - return datas - } - - sort.Slice(ports, func(i, j int) bool { - return ports[i].PrivatePort < ports[j].PrivatePort - }) - start := ports[0] - - for i := 1; i < len(ports); i++ { - if ports[i].PrivatePort != ports[i-1].PrivatePort+1 || ports[i].IP != ports[i-1].IP || ports[i].PublicPort != ports[i-1].PublicPort+1 || ports[i].Type != ports[i-1].Type { - if ports[i-1].PrivatePort == start.PrivatePort { - itemPortStr := fmt.Sprintf("%s:%v/%s", start.IP, start.PrivatePort, start.Type) - if start.PublicPort != 0 { - itemPortStr = fmt.Sprintf("%s:%v->%v/%s", start.IP, start.PublicPort, start.PrivatePort, start.Type) - } - if len(start.IP) == 0 { - itemPortStr = strings.TrimPrefix(itemPortStr, ":") - } - datas = append(datas, itemPortStr) - } else { - itemPortStr := fmt.Sprintf("%s:%v-%v/%s", start.IP, start.PrivatePort, ports[i-1].PrivatePort, start.Type) - if start.PublicPort != 0 { - itemPortStr = fmt.Sprintf("%s:%v-%v->%v-%v/%s", start.IP, start.PublicPort, ports[i-1].PublicPort, start.PrivatePort, ports[i-1].PrivatePort, start.Type) - } - if len(start.IP) == 0 { - itemPortStr = strings.TrimPrefix(itemPortStr, ":") - } - datas = append(datas, itemPortStr) - } - start = ports[i] - } - if i == len(ports)-1 { - if ports[i].PrivatePort == start.PrivatePort { - itemPortStr := fmt.Sprintf("%s:%v/%s", start.IP, start.PrivatePort, start.Type) - if start.PublicPort != 0 { - itemPortStr = fmt.Sprintf("%s:%v->%v/%s", start.IP, start.PublicPort, start.PrivatePort, start.Type) - } - if len(start.IP) == 0 { - itemPortStr = strings.TrimPrefix(itemPortStr, ":") - } - datas = append(datas, itemPortStr) - } else { - itemPortStr := fmt.Sprintf("%s:%v-%v/%s", start.IP, start.PrivatePort, ports[i].PrivatePort, start.Type) - if start.PublicPort != 0 { - itemPortStr = fmt.Sprintf("%s:%v-%v->%v-%v/%s", start.IP, start.PublicPort, ports[i].PublicPort, start.PrivatePort, ports[i].PrivatePort, start.Type) - } - if len(start.IP) == 0 { - itemPortStr = strings.TrimPrefix(itemPortStr, ":") - } - datas = append(datas, itemPortStr) - } - } - } - return datas -} diff --git a/backend/app/service/container_compose.go b/backend/app/service/container_compose.go deleted file mode 100644 index 565abf292..000000000 --- a/backend/app/service/container_compose.go +++ /dev/null @@ -1,282 +0,0 @@ -package service - -import ( - "bufio" - "errors" - "fmt" - "io" - "os" - "os/exec" - "path" - "sort" - "strings" - "time" - - "github.com/docker/docker/api/types/container" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/compose" - "github.com/1Panel-dev/1Panel/backend/utils/docker" - "github.com/docker/docker/api/types/filters" - "golang.org/x/net/context" -) - -const composeProjectLabel = "com.docker.compose.project" -const composeConfigLabel = "com.docker.compose.project.config_files" -const composeWorkdirLabel = "com.docker.compose.project.working_dir" -const composeCreatedBy = "createdBy" - -func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface{}, error) { - var ( - records []dto.ComposeInfo - BackDatas []dto.ComposeInfo - ) - client, err := docker.NewDockerClient() - if err != nil { - return 0, nil, err - } - defer client.Close() - - options := container.ListOptions{All: true} - options.Filters = filters.NewArgs() - options.Filters.Add("label", composeProjectLabel) - - list, err := client.ContainerList(context.Background(), options) - if err != nil { - return 0, nil, err - } - - composeCreatedByLocal, _ := composeRepo.ListRecord() - composeMap := make(map[string]dto.ComposeInfo) - for _, container := range list { - if name, ok := container.Labels[composeProjectLabel]; ok { - containerItem := dto.ComposeContainer{ - ContainerID: container.ID, - Name: container.Names[0][1:], - State: container.State, - CreateTime: time.Unix(container.Created, 0).Format(constant.DateTimeLayout), - } - if compose, has := composeMap[name]; has { - compose.ContainerNumber++ - compose.Containers = append(compose.Containers, containerItem) - composeMap[name] = compose - } else { - config := container.Labels[composeConfigLabel] - workdir := container.Labels[composeWorkdirLabel] - composeItem := dto.ComposeInfo{ - ContainerNumber: 1, - CreatedAt: time.Unix(container.Created, 0).Format(constant.DateTimeLayout), - ConfigFile: config, - Workdir: workdir, - Containers: []dto.ComposeContainer{containerItem}, - } - createdBy, ok := container.Labels[composeCreatedBy] - if ok { - composeItem.CreatedBy = createdBy - } - if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) { - composeItem.Path = config - } else { - composeItem.Path = workdir - } - for i := 0; i < len(composeCreatedByLocal); i++ { - if composeCreatedByLocal[i].Name == name { - composeItem.CreatedBy = "1Panel" - composeCreatedByLocal = append(composeCreatedByLocal[:i], composeCreatedByLocal[i+1:]...) - break - } - } - composeMap[name] = composeItem - } - } - } - for _, item := range composeCreatedByLocal { - if err := composeRepo.DeleteRecord(commonRepo.WithByID(item.ID)); err != nil { - global.LOG.Error(err) - } - } - for key, value := range composeMap { - value.Name = key - records = append(records, value) - } - if len(req.Info) != 0 { - length, count := len(records), 0 - for count < length { - if !strings.Contains(records[count].Name, req.Info) { - records = append(records[:count], records[(count+1):]...) - length-- - } else { - count++ - } - } - } - sort.Slice(records, func(i, j int) bool { - return records[i].CreatedAt > records[j].CreatedAt - }) - total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize - if start > total { - BackDatas = make([]dto.ComposeInfo, 0) - } else { - if end >= total { - end = total - } - BackDatas = records[start:end] - } - return int64(total), BackDatas, nil -} - -func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) { - if cmd.CheckIllegal(req.Path) { - return false, buserr.New(constant.ErrCmdIllegal) - } - composeItem, _ := composeRepo.GetRecord(commonRepo.WithByName(req.Name)) - if composeItem.ID != 0 { - return false, constant.ErrRecordExist - } - if err := u.loadPath(&req); err != nil { - return false, err - } - cmd := exec.Command("docker-compose", "-f", req.Path, "config") - stdout, err := cmd.CombinedOutput() - if err != nil { - return false, errors.New(string(stdout)) - } - return true, nil -} - -func (u *ContainerService) CreateCompose(req dto.ComposeCreate) (string, error) { - if cmd.CheckIllegal(req.Name, req.Path) { - return "", buserr.New(constant.ErrCmdIllegal) - } - if err := u.loadPath(&req); err != nil { - return "", err - } - global.LOG.Infof("docker-compose.yml %s create successful, start to docker-compose up", req.Name) - - if req.From == "path" { - req.Name = path.Base(path.Dir(req.Path)) - } - - dockerLogDir := path.Join(global.CONF.System.TmpDir, "docker_logs") - if _, err := os.Stat(dockerLogDir); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(dockerLogDir, os.ModePerm); err != nil { - return "", err - } - } - logItem := fmt.Sprintf("%s/compose_create_%s_%s.log", dockerLogDir, req.Name, time.Now().Format(constant.DateTimeSlimLayout)) - file, err := os.OpenFile(logItem, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) - if err != nil { - return "", err - } - go func() { - defer file.Close() - cmd := exec.Command("docker-compose", "-f", req.Path, "up", "-d") - multiWriter := io.MultiWriter(os.Stdout, file) - cmd.Stdout = multiWriter - cmd.Stderr = multiWriter - if err := cmd.Run(); err != nil { - global.LOG.Errorf("docker-compose up %s failed, err: %v", req.Name, err) - _, _ = compose.Down(req.Path) - _, _ = file.WriteString("docker-compose up failed!") - return - } - global.LOG.Infof("docker-compose up %s successful!", req.Name) - _ = composeRepo.CreateRecord(&model.Compose{Name: req.Name}) - _, _ = file.WriteString("docker-compose up successful!") - }() - - return path.Base(logItem), nil -} - -func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error { - if cmd.CheckIllegal(req.Path, req.Operation) { - return buserr.New(constant.ErrCmdIllegal) - } - if _, err := os.Stat(req.Path); err != nil { - return fmt.Errorf("load file with path %s failed, %v", req.Path, err) - } - if stdout, err := compose.Operate(req.Path, req.Operation); err != nil { - return errors.New(string(stdout)) - } - global.LOG.Infof("docker-compose %s %s successful", req.Operation, req.Name) - if req.Operation == "down" { - _ = composeRepo.DeleteRecord(commonRepo.WithByName(req.Name)) - if req.WithFile { - _ = os.RemoveAll(path.Dir(req.Path)) - } - } - - return nil -} - -func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error { - if cmd.CheckIllegal(req.Name, req.Path) { - return buserr.New(constant.ErrCmdIllegal) - } - oldFile, err := os.ReadFile(req.Path) - if err != nil { - return fmt.Errorf("load file with path %s failed, %v", req.Path, err) - } - file, err := os.OpenFile(req.Path, os.O_WRONLY|os.O_TRUNC, 0640) - if err != nil { - return err - } - defer file.Close() - write := bufio.NewWriter(file) - _, _ = write.WriteString(req.Content) - write.Flush() - - global.LOG.Infof("docker-compose.yml %s has been replaced, now start to docker-compose restart", req.Path) - if stdout, err := compose.Up(req.Path); err != nil { - if err := recreateCompose(string(oldFile), req.Path); err != nil { - return fmt.Errorf("update failed when handle compose up, err: %s, recreate failed: %v", string(stdout), err) - } - return fmt.Errorf("update failed when handle compose up, err: %s", string(stdout)) - } - - return nil -} - -func (u *ContainerService) loadPath(req *dto.ComposeCreate) error { - if req.From == "template" || req.From == "edit" { - dir := fmt.Sprintf("%s/docker/compose/%s", constant.DataDir, req.Name) - if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(dir, os.ModePerm); err != nil { - return err - } - } - - path := fmt.Sprintf("%s/docker-compose.yml", dir) - file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) - if err != nil { - return err - } - defer file.Close() - write := bufio.NewWriter(file) - _, _ = write.WriteString(string(req.File)) - write.Flush() - req.Path = path - } - return nil -} - -func recreateCompose(content, path string) error { - file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640) - if err != nil { - return err - } - defer file.Close() - write := bufio.NewWriter(file) - _, _ = write.WriteString(content) - write.Flush() - - if stdout, err := compose.Up(path); err != nil { - return errors.New(string(stdout)) - } - return nil -} diff --git a/backend/app/service/container_network.go b/backend/app/service/container_network.go deleted file mode 100644 index 44424e914..000000000 --- a/backend/app/service/container_network.go +++ /dev/null @@ -1,179 +0,0 @@ -package service - -import ( - "context" - "fmt" - "sort" - "strings" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/docker" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/network" -) - -func (u *ContainerService) PageNetwork(req dto.SearchWithPage) (int64, interface{}, error) { - client, err := docker.NewDockerClient() - if err != nil { - return 0, nil, err - } - defer client.Close() - list, err := client.NetworkList(context.TODO(), types.NetworkListOptions{}) - if err != nil { - return 0, nil, err - } - if len(req.Info) != 0 { - length, count := len(list), 0 - for count < length { - if !strings.Contains(list[count].Name, req.Info) { - list = append(list[:count], list[(count+1):]...) - length-- - } else { - count++ - } - } - } - var ( - data []dto.Network - records []types.NetworkResource - ) - sort.Slice(list, func(i, j int) bool { - return list[i].Created.Before(list[j].Created) - }) - total, start, end := len(list), (req.Page-1)*req.PageSize, req.Page*req.PageSize - if start > total { - records = make([]types.NetworkResource, 0) - } else { - if end >= total { - end = total - } - records = list[start:end] - } - - for _, item := range records { - tag := make([]string, 0) - for key, val := range item.Labels { - tag = append(tag, fmt.Sprintf("%s=%s", key, val)) - } - var ipam network.IPAMConfig - if len(item.IPAM.Config) > 0 { - ipam = item.IPAM.Config[0] - } - data = append(data, dto.Network{ - ID: item.ID, - CreatedAt: item.Created, - Name: item.Name, - Driver: item.Driver, - IPAMDriver: item.IPAM.Driver, - Subnet: ipam.Subnet, - Gateway: ipam.Gateway, - Attachable: item.Attachable, - Labels: tag, - }) - } - - return int64(total), data, nil -} - -func (u *ContainerService) ListNetwork() ([]dto.Options, error) { - client, err := docker.NewDockerClient() - if err != nil { - return nil, err - } - defer client.Close() - list, err := client.NetworkList(context.TODO(), types.NetworkListOptions{}) - if err != nil { - return nil, err - } - var datas []dto.Options - for _, item := range list { - datas = append(datas, dto.Options{Option: item.Name}) - } - sort.Slice(datas, func(i, j int) bool { - return datas[i].Option < datas[j].Option - }) - return datas, nil -} - -func (u *ContainerService) DeleteNetwork(req dto.BatchDelete) error { - client, err := docker.NewDockerClient() - if err != nil { - return err - } - defer client.Close() - for _, id := range req.Names { - if err := client.NetworkRemove(context.TODO(), id); err != nil { - if strings.Contains(err.Error(), "has active endpoints") { - return buserr.WithDetail(constant.ErrInUsed, id, nil) - } - return err - } - } - return nil -} -func (u *ContainerService) CreateNetwork(req dto.NetworkCreate) error { - client, err := docker.NewDockerClient() - if err != nil { - return err - } - defer client.Close() - var ( - ipams []network.IPAMConfig - enableV6 bool - ) - if req.Ipv4 { - var itemIpam network.IPAMConfig - if len(req.AuxAddress) != 0 { - itemIpam.AuxAddress = make(map[string]string) - } - if len(req.Subnet) != 0 { - itemIpam.Subnet = req.Subnet - } - if len(req.Gateway) != 0 { - itemIpam.Gateway = req.Gateway - } - if len(req.IPRange) != 0 { - itemIpam.IPRange = req.IPRange - } - for _, addr := range req.AuxAddress { - itemIpam.AuxAddress[addr.Key] = addr.Value - } - ipams = append(ipams, itemIpam) - } - if req.Ipv6 { - enableV6 = true - var itemIpam network.IPAMConfig - if len(req.AuxAddress) != 0 { - itemIpam.AuxAddress = make(map[string]string) - } - if len(req.SubnetV6) != 0 { - itemIpam.Subnet = req.SubnetV6 - } - if len(req.GatewayV6) != 0 { - itemIpam.Gateway = req.GatewayV6 - } - if len(req.IPRangeV6) != 0 { - itemIpam.IPRange = req.IPRangeV6 - } - for _, addr := range req.AuxAddressV6 { - itemIpam.AuxAddress[addr.Key] = addr.Value - } - ipams = append(ipams, itemIpam) - } - - options := types.NetworkCreate{ - EnableIPv6: enableV6, - Driver: req.Driver, - Options: stringsToMap(req.Options), - Labels: stringsToMap(req.Labels), - } - if len(ipams) != 0 { - options.IPAM = &network.IPAM{Config: ipams} - } - if _, err := client.NetworkCreate(context.TODO(), req.Name, options); err != nil { - return err - } - return nil -} diff --git a/backend/app/service/container_volume.go b/backend/app/service/container_volume.go deleted file mode 100644 index daaa49b4c..000000000 --- a/backend/app/service/container_volume.go +++ /dev/null @@ -1,143 +0,0 @@ -package service - -import ( - "context" - "sort" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/1Panel-dev/1Panel/backend/utils/docker" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/volume" -) - -func (u *ContainerService) PageVolume(req dto.SearchWithPage) (int64, interface{}, error) { - client, err := docker.NewDockerClient() - if err != nil { - return 0, nil, err - } - list, err := client.VolumeList(context.TODO(), volume.ListOptions{}) - if err != nil { - return 0, nil, err - } - if len(req.Info) != 0 { - length, count := len(list.Volumes), 0 - for count < length { - if !strings.Contains(list.Volumes[count].Name, req.Info) { - list.Volumes = append(list.Volumes[:count], list.Volumes[(count+1):]...) - length-- - } else { - count++ - } - } - } - var ( - data []dto.Volume - records []*volume.Volume - ) - sort.Slice(list.Volumes, func(i, j int) bool { - return list.Volumes[i].CreatedAt > list.Volumes[j].CreatedAt - }) - total, start, end := len(list.Volumes), (req.Page-1)*req.PageSize, req.Page*req.PageSize - if start > total { - records = make([]*volume.Volume, 0) - } else { - if end >= total { - end = total - } - records = list.Volumes[start:end] - } - - nyc, _ := time.LoadLocation(common.LoadTimeZone()) - for _, item := range records { - tag := make([]string, 0) - for _, val := range item.Labels { - tag = append(tag, val) - } - var createTime time.Time - if strings.Contains(item.CreatedAt, "Z") { - createTime, _ = time.ParseInLocation("2006-01-02T15:04:05Z", item.CreatedAt, nyc) - } else if strings.Contains(item.CreatedAt, "+") { - createTime, _ = time.ParseInLocation("2006-01-02T15:04:05+08:00", item.CreatedAt, nyc) - } else { - createTime, _ = time.ParseInLocation("2006-01-02T15:04:05", item.CreatedAt, nyc) - } - data = append(data, dto.Volume{ - CreatedAt: createTime, - Name: item.Name, - Driver: item.Driver, - Mountpoint: item.Mountpoint, - Labels: tag, - }) - } - - return int64(total), data, nil -} -func (u *ContainerService) ListVolume() ([]dto.Options, error) { - client, err := docker.NewDockerClient() - if err != nil { - return nil, err - } - defer client.Close() - list, err := client.VolumeList(context.TODO(), volume.ListOptions{}) - if err != nil { - return nil, err - } - var datas []dto.Options - for _, item := range list.Volumes { - datas = append(datas, dto.Options{ - Option: item.Name, - }) - } - sort.Slice(datas, func(i, j int) bool { - return datas[i].Option < datas[j].Option - }) - return datas, nil -} -func (u *ContainerService) DeleteVolume(req dto.BatchDelete) error { - client, err := docker.NewDockerClient() - if err != nil { - return err - } - defer client.Close() - for _, id := range req.Names { - if err := client.VolumeRemove(context.TODO(), id, true); err != nil { - if strings.Contains(err.Error(), "volume is in use") { - return buserr.WithDetail(constant.ErrInUsed, id, nil) - } - return err - } - } - return nil -} -func (u *ContainerService) CreateVolume(req dto.VolumeCreate) error { - client, err := docker.NewDockerClient() - if err != nil { - return err - } - defer client.Close() - arg := filters.NewArgs() - arg.Add("name", req.Name) - vos, _ := client.VolumeList(context.TODO(), volume.ListOptions{Filters: arg}) - if len(vos.Volumes) != 0 { - for _, v := range vos.Volumes { - if v.Name == req.Name { - return constant.ErrRecordExist - } - } - } - options := volume.CreateOptions{ - Name: req.Name, - Driver: req.Driver, - DriverOpts: stringsToMap(req.Options), - Labels: stringsToMap(req.Labels), - } - if _, err := client.VolumeCreate(context.TODO(), options); err != nil { - return err - } - return nil -} diff --git a/backend/app/service/cronjob.go b/backend/app/service/cronjob.go deleted file mode 100644 index 41accecf4..000000000 --- a/backend/app/service/cronjob.go +++ /dev/null @@ -1,344 +0,0 @@ -package service - -import ( - "bufio" - "fmt" - "os" - "path" - "strconv" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/jinzhu/copier" - "github.com/pkg/errors" - "github.com/robfig/cron/v3" -) - -type CronjobService struct{} - -type ICronjobService interface { - SearchWithPage(search dto.PageCronjob) (int64, interface{}, error) - SearchRecords(search dto.SearchRecord) (int64, interface{}, error) - Create(cronjobDto dto.CronjobCreate) error - HandleOnce(id uint) error - Update(id uint, req dto.CronjobUpdate) error - UpdateStatus(id uint, status string) error - Delete(req dto.CronjobBatchDelete) error - Download(down dto.CronjobDownload) (string, error) - StartJob(cronjob *model.Cronjob, isUpdate bool) (string, error) - CleanRecord(req dto.CronjobClean) error - - LoadRecordLog(req dto.OperateByID) string -} - -func NewICronjobService() ICronjobService { - return &CronjobService{} -} - -func (u *CronjobService) SearchWithPage(search dto.PageCronjob) (int64, interface{}, error) { - total, cronjobs, err := cronjobRepo.Page(search.Page, search.PageSize, commonRepo.WithLikeName(search.Info), commonRepo.WithOrderRuleBy(search.OrderBy, search.Order)) - var dtoCronjobs []dto.CronjobInfo - for _, cronjob := range cronjobs { - var item dto.CronjobInfo - if err := copier.Copy(&item, &cronjob); err != nil { - return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - record, _ := cronjobRepo.RecordFirst(cronjob.ID) - if record.ID != 0 { - item.LastRecordTime = record.StartTime.Format(constant.DateTimeLayout) - } else { - item.LastRecordTime = "-" - } - dtoCronjobs = append(dtoCronjobs, item) - } - return total, dtoCronjobs, err -} - -func (u *CronjobService) SearchRecords(search dto.SearchRecord) (int64, interface{}, error) { - total, records, err := cronjobRepo.PageRecords( - search.Page, - search.PageSize, - commonRepo.WithByStatus(search.Status), - cronjobRepo.WithByJobID(search.CronjobID), - commonRepo.WithByDate(search.StartTime, search.EndTime)) - var dtoCronjobs []dto.Record - for _, record := range records { - var item dto.Record - if err := copier.Copy(&item, &record); err != nil { - return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - item.StartTime = record.StartTime.Format(constant.DateTimeLayout) - dtoCronjobs = append(dtoCronjobs, item) - } - return total, dtoCronjobs, err -} - -func (u *CronjobService) LoadRecordLog(req dto.OperateByID) string { - record, err := cronjobRepo.GetRecord(commonRepo.WithByID(req.ID)) - if err != nil { - return "" - } - if _, err := os.Stat(record.Records); err != nil { - return "" - } - content, err := os.ReadFile(record.Records) - if err != nil { - return "" - } - return string(content) -} - -func (u *CronjobService) CleanRecord(req dto.CronjobClean) error { - cronjob, err := cronjobRepo.Get(commonRepo.WithByID(req.CronjobID)) - if err != nil { - return err - } - if req.CleanData { - if hasBackup(cronjob.Type) { - accountMap, err := loadClientMap(cronjob.BackupAccounts) - if err != nil { - return err - } - cronjob.RetainCopies = 0 - u.removeExpiredBackup(cronjob, accountMap, model.BackupRecord{}) - } else { - u.removeExpiredLog(cronjob) - } - } - if req.IsDelete { - records, _ := backupRepo.ListRecord(backupRepo.WithByCronID(cronjob.ID)) - for _, records := range records { - records.CronjobID = 0 - _ = backupRepo.UpdateRecord(&records) - } - } - delRecords, err := cronjobRepo.ListRecord(cronjobRepo.WithByJobID(int(req.CronjobID))) - if err != nil { - return err - } - for _, del := range delRecords { - _ = os.RemoveAll(del.Records) - } - if err := cronjobRepo.DeleteRecord(cronjobRepo.WithByJobID(int(req.CronjobID))); err != nil { - return err - } - return nil -} - -func (u *CronjobService) Download(down dto.CronjobDownload) (string, error) { - record, _ := cronjobRepo.GetRecord(commonRepo.WithByID(down.RecordID)) - if record.ID == 0 { - return "", constant.ErrRecordNotFound - } - backup, _ := backupRepo.Get(commonRepo.WithByID(down.BackupAccountID)) - if backup.ID == 0 { - return "", constant.ErrRecordNotFound - } - if backup.Type == "LOCAL" || record.FromLocal { - if _, err := os.Stat(record.File); err != nil && os.IsNotExist(err) { - return "", err - } - return record.File, nil - } - tempPath := fmt.Sprintf("%s/download/%s", constant.DataDir, record.File) - if _, err := os.Stat(tempPath); err != nil && os.IsNotExist(err) { - client, err := NewIBackupService().NewClient(&backup) - if err != nil { - return "", err - } - _ = os.MkdirAll(path.Dir(tempPath), os.ModePerm) - isOK, err := client.Download(record.File, tempPath) - if !isOK || err != nil { - return "", err - } - } - return tempPath, nil -} - -func (u *CronjobService) HandleOnce(id uint) error { - cronjob, _ := cronjobRepo.Get(commonRepo.WithByID(id)) - if cronjob.ID == 0 { - return constant.ErrRecordNotFound - } - u.HandleJob(&cronjob) - return nil -} - -func (u *CronjobService) Create(cronjobDto dto.CronjobCreate) error { - cronjob, _ := cronjobRepo.Get(commonRepo.WithByName(cronjobDto.Name)) - if cronjob.ID != 0 { - return constant.ErrRecordExist - } - cronjob.Secret = cronjobDto.Secret - if err := copier.Copy(&cronjob, &cronjobDto); err != nil { - return errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - cronjob.Status = constant.StatusEnable - - global.LOG.Infof("create cronjob %s successful, spec: %s", cronjob.Name, cronjob.Spec) - spec := cronjob.Spec - entryIDs, err := u.StartJob(&cronjob, false) - if err != nil { - return err - } - cronjob.Spec = spec - cronjob.EntryIDs = entryIDs - if err := cronjobRepo.Create(&cronjob); err != nil { - return err - } - return nil -} - -func (u *CronjobService) StartJob(cronjob *model.Cronjob, isUpdate bool) (string, error) { - if len(cronjob.EntryIDs) != 0 && isUpdate { - ids := strings.Split(cronjob.EntryIDs, ",") - for _, id := range ids { - idItem, _ := strconv.Atoi(id) - global.Cron.Remove(cron.EntryID(idItem)) - } - } - specs := strings.Split(cronjob.Spec, ",") - var ids []string - for _, spec := range specs { - cronjob.Spec = spec - entryID, err := u.AddCronJob(cronjob) - if err != nil { - return "", err - } - ids = append(ids, fmt.Sprintf("%v", entryID)) - } - return strings.Join(ids, ","), nil -} - -func (u *CronjobService) Delete(req dto.CronjobBatchDelete) error { - for _, id := range req.IDs { - cronjob, _ := cronjobRepo.Get(commonRepo.WithByID(id)) - if cronjob.ID == 0 { - return errors.New("find cronjob in db failed") - } - ids := strings.Split(cronjob.EntryIDs, ",") - for _, id := range ids { - idItem, _ := strconv.Atoi(id) - global.Cron.Remove(cron.EntryID(idItem)) - } - global.LOG.Infof("stop cronjob entryID: %s", cronjob.EntryIDs) - if err := u.CleanRecord(dto.CronjobClean{CronjobID: id, CleanData: req.CleanData, IsDelete: true}); err != nil { - return err - } - if err := cronjobRepo.Delete(commonRepo.WithByID(id)); err != nil { - return err - } - } - - return nil -} - -func (u *CronjobService) Update(id uint, req dto.CronjobUpdate) error { - var cronjob model.Cronjob - if err := copier.Copy(&cronjob, &req); err != nil { - return errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - cronModel, err := cronjobRepo.Get(commonRepo.WithByID(id)) - if err != nil { - return constant.ErrRecordNotFound - } - upMap := make(map[string]interface{}) - cronjob.EntryIDs = cronModel.EntryIDs - cronjob.Type = cronModel.Type - spec := cronjob.Spec - if cronModel.Status == constant.StatusEnable { - newEntryIDs, err := u.StartJob(&cronjob, true) - if err != nil { - return err - } - upMap["entry_ids"] = newEntryIDs - } else { - ids := strings.Split(cronjob.EntryIDs, ",") - for _, id := range ids { - idItem, _ := strconv.Atoi(id) - global.Cron.Remove(cron.EntryID(idItem)) - } - } - - upMap["name"] = req.Name - upMap["spec"] = spec - upMap["script"] = req.Script - upMap["command"] = req.Command - upMap["container_name"] = req.ContainerName - upMap["app_id"] = req.AppID - upMap["website"] = req.Website - upMap["exclusion_rules"] = req.ExclusionRules - upMap["db_type"] = req.DBType - upMap["db_name"] = req.DBName - upMap["url"] = req.URL - upMap["source_dir"] = req.SourceDir - - upMap["backup_accounts"] = req.BackupAccounts - upMap["default_download"] = req.DefaultDownload - upMap["retain_copies"] = req.RetainCopies - upMap["secret"] = req.Secret - return cronjobRepo.Update(id, upMap) -} - -func (u *CronjobService) UpdateStatus(id uint, status string) error { - cronjob, _ := cronjobRepo.Get(commonRepo.WithByID(id)) - if cronjob.ID == 0 { - return errors.WithMessage(constant.ErrRecordNotFound, "record not found") - } - var ( - entryIDs string - err error - ) - if status == constant.StatusEnable { - entryIDs, err = u.StartJob(&cronjob, false) - if err != nil { - return err - } - } else { - ids := strings.Split(cronjob.EntryIDs, ",") - for _, id := range ids { - idItem, _ := strconv.Atoi(id) - global.Cron.Remove(cron.EntryID(idItem)) - } - global.LOG.Infof("stop cronjob entryID: %s", cronjob.EntryIDs) - } - return cronjobRepo.Update(cronjob.ID, map[string]interface{}{"status": status, "entry_ids": entryIDs}) -} - -func (u *CronjobService) AddCronJob(cronjob *model.Cronjob) (int, error) { - addFunc := func() { - u.HandleJob(cronjob) - } - global.LOG.Infof("add %s job %s successful", cronjob.Type, cronjob.Name) - entryID, err := global.Cron.AddFunc(cronjob.Spec, addFunc) - if err != nil { - return 0, err - } - global.LOG.Infof("start cronjob entryID: %d", entryID) - return int(entryID), nil -} - -func mkdirAndWriteFile(cronjob *model.Cronjob, startTime time.Time, msg []byte) (string, error) { - dir := fmt.Sprintf("%s/task/%s/%s", constant.DataDir, cronjob.Type, cronjob.Name) - if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(dir, os.ModePerm); err != nil { - return "", err - } - } - - path := fmt.Sprintf("%s/%s.log", dir, startTime.Format(constant.DateTimeSlimLayout)) - global.LOG.Infof("cronjob %s has generated some logs %s", cronjob.Name, path) - file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - return "", err - } - defer file.Close() - write := bufio.NewWriter(file) - _, _ = write.WriteString(string(msg)) - write.Flush() - return path, nil -} diff --git a/backend/app/service/cronjob_backup.go b/backend/app/service/cronjob_backup.go deleted file mode 100644 index e77f94075..000000000 --- a/backend/app/service/cronjob_backup.go +++ /dev/null @@ -1,386 +0,0 @@ -package service - -import ( - "fmt" - "os" - "path" - "strconv" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/common" -) - -func (u *CronjobService) handleApp(cronjob model.Cronjob, startTime time.Time) error { - var apps []model.AppInstall - if cronjob.AppID == "all" { - apps, _ = appInstallRepo.ListBy() - } else { - itemID, _ := strconv.Atoi(cronjob.AppID) - app, err := appInstallRepo.GetFirst(commonRepo.WithByID(uint(itemID))) - if err != nil { - return err - } - apps = append(apps, app) - } - accountMap, err := loadClientMap(cronjob.BackupAccounts) - if err != nil { - return err - } - for _, app := range apps { - var record model.BackupRecord - record.From = "cronjob" - record.Type = "app" - record.CronjobID = cronjob.ID - record.Name = app.App.Key - record.DetailName = app.Name - record.Source, record.BackupType = loadRecordPath(cronjob, accountMap) - backupDir := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("app/%s/%s", app.App.Key, app.Name)) - record.FileName = fmt.Sprintf("app_%s_%s.tar.gz", app.Name, startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5)) - if err := handleAppBackup(&app, backupDir, record.FileName, cronjob.ExclusionRules, cronjob.Secret); err != nil { - return err - } - downloadPath, err := u.uploadCronjobBackFile(cronjob, accountMap, path.Join(backupDir, record.FileName)) - if err != nil { - return err - } - record.FileDir = path.Dir(downloadPath) - if err := backupRepo.CreateRecord(&record); err != nil { - global.LOG.Errorf("save backup record failed, err: %v", err) - return err - } - u.removeExpiredBackup(cronjob, accountMap, record) - } - return nil -} - -func (u *CronjobService) handleWebsite(cronjob model.Cronjob, startTime time.Time) error { - webs := loadWebsForJob(cronjob) - accountMap, err := loadClientMap(cronjob.BackupAccounts) - if err != nil { - return err - } - for _, web := range webs { - var record model.BackupRecord - record.From = "cronjob" - record.Type = "website" - record.CronjobID = cronjob.ID - record.Name = web.PrimaryDomain - record.DetailName = web.Alias - record.Source, record.BackupType = loadRecordPath(cronjob, accountMap) - backupDir := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("website/%s", web.PrimaryDomain)) - record.FileName = fmt.Sprintf("website_%s_%s.tar.gz", web.PrimaryDomain, startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5)) - if err := handleWebsiteBackup(&web, backupDir, record.FileName, cronjob.ExclusionRules, cronjob.Secret); err != nil { - return err - } - downloadPath, err := u.uploadCronjobBackFile(cronjob, accountMap, path.Join(backupDir, record.FileName)) - if err != nil { - return err - } - record.FileDir = path.Dir(downloadPath) - if err := backupRepo.CreateRecord(&record); err != nil { - global.LOG.Errorf("save backup record failed, err: %v", err) - return err - } - u.removeExpiredBackup(cronjob, accountMap, record) - } - return nil -} - -func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Time) error { - dbs := loadDbsForJob(cronjob) - accountMap, err := loadClientMap(cronjob.BackupAccounts) - if err != nil { - return err - } - for _, dbInfo := range dbs { - var record model.BackupRecord - record.From = "cronjob" - record.Type = dbInfo.DBType - record.CronjobID = cronjob.ID - record.Name = dbInfo.Database - record.DetailName = dbInfo.Name - record.Source, record.BackupType = loadRecordPath(cronjob, accountMap) - - backupDir := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("database/%s/%s/%s", dbInfo.DBType, record.Name, dbInfo.Name)) - record.FileName = fmt.Sprintf("db_%s_%s.sql.gz", dbInfo.Name, startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5)) - if cronjob.DBType == "mysql" || cronjob.DBType == "mariadb" { - if err := handleMysqlBackup(dbInfo.Database, dbInfo.DBType, dbInfo.Name, backupDir, record.FileName); err != nil { - return err - } - } else { - if err := handlePostgresqlBackup(dbInfo.Database, dbInfo.Name, backupDir, record.FileName); err != nil { - return err - } - } - downloadPath, err := u.uploadCronjobBackFile(cronjob, accountMap, path.Join(backupDir, record.FileName)) - if err != nil { - return err - } - record.FileDir = path.Dir(downloadPath) - if err := backupRepo.CreateRecord(&record); err != nil { - global.LOG.Errorf("save backup record failed, err: %v", err) - return err - } - u.removeExpiredBackup(cronjob, accountMap, record) - } - return nil -} - -func (u *CronjobService) handleDirectory(cronjob model.Cronjob, startTime time.Time) error { - accountMap, err := loadClientMap(cronjob.BackupAccounts) - if err != nil { - return err - } - fileName := fmt.Sprintf("directory%s_%s.tar.gz", strings.ReplaceAll(cronjob.SourceDir, "/", "_"), startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5)) - backupDir := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("%s/%s", cronjob.Type, cronjob.Name)) - if err := handleTar(cronjob.SourceDir, backupDir, fileName, cronjob.ExclusionRules, cronjob.Secret); err != nil { - return err - } - var record model.BackupRecord - record.From = "cronjob" - record.Type = "directory" - record.CronjobID = cronjob.ID - record.Name = cronjob.Name - record.Source, record.BackupType = loadRecordPath(cronjob, accountMap) - downloadPath, err := u.uploadCronjobBackFile(cronjob, accountMap, path.Join(backupDir, fileName)) - if err != nil { - return err - } - record.FileDir = path.Dir(downloadPath) - record.FileName = fileName - if err := backupRepo.CreateRecord(&record); err != nil { - global.LOG.Errorf("save backup record failed, err: %v", err) - return err - } - u.removeExpiredBackup(cronjob, accountMap, record) - return nil -} - -func (u *CronjobService) handleSystemLog(cronjob model.Cronjob, startTime time.Time) error { - accountMap, err := loadClientMap(cronjob.BackupAccounts) - if err != nil { - return err - } - nameItem := startTime.Format(constant.DateTimeSlimLayout) + common.RandStrAndNum(5) - fileName := fmt.Sprintf("system_log_%s.tar.gz", nameItem) - backupDir := path.Join(global.CONF.System.TmpDir, "log", nameItem) - if err := handleBackupLogs(backupDir, fileName, cronjob.Secret); err != nil { - return err - } - var record model.BackupRecord - record.From = "cronjob" - record.Type = "log" - record.CronjobID = cronjob.ID - record.Name = cronjob.Name - record.Source, record.BackupType = loadRecordPath(cronjob, accountMap) - downloadPath, err := u.uploadCronjobBackFile(cronjob, accountMap, path.Join(path.Dir(backupDir), fileName)) - if err != nil { - return err - } - record.FileDir = path.Dir(downloadPath) - record.FileName = fileName - if err := backupRepo.CreateRecord(&record); err != nil { - global.LOG.Errorf("save backup record failed, err: %v", err) - return err - } - u.removeExpiredBackup(cronjob, accountMap, record) - return nil -} - -func (u *CronjobService) handleSnapshot(cronjob model.Cronjob, startTime time.Time, logPath string) error { - accountMap, err := loadClientMap(cronjob.BackupAccounts) - if err != nil { - return err - } - - var record model.BackupRecord - record.From = "cronjob" - record.Type = "directory" - record.CronjobID = cronjob.ID - record.Name = cronjob.Name - record.Source, record.BackupType = loadRecordPath(cronjob, accountMap) - record.FileDir = "system_snapshot" - - req := dto.SnapshotCreate{ - From: record.BackupType, - DefaultDownload: cronjob.DefaultDownload, - } - name, err := NewISnapshotService().HandleSnapshot(true, logPath, req, startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5), cronjob.Secret) - if err != nil { - return err - } - record.FileName = name + ".tar.gz" - - if err := backupRepo.CreateRecord(&record); err != nil { - global.LOG.Errorf("save backup record failed, err: %v", err) - return err - } - u.removeExpiredBackup(cronjob, accountMap, record) - return nil -} - -type databaseHelper struct { - DBType string - Database string - Name string -} - -func loadDbsForJob(cronjob model.Cronjob) []databaseHelper { - var dbs []databaseHelper - if cronjob.DBName == "all" { - if cronjob.DBType == "mysql" || cronjob.DBType == "mariadb" { - mysqlItems, _ := mysqlRepo.List() - for _, mysql := range mysqlItems { - dbs = append(dbs, databaseHelper{ - DBType: cronjob.DBType, - Database: mysql.MysqlName, - Name: mysql.Name, - }) - } - } else { - pgItems, _ := postgresqlRepo.List() - for _, pg := range pgItems { - dbs = append(dbs, databaseHelper{ - DBType: cronjob.DBType, - Database: pg.PostgresqlName, - Name: pg.Name, - }) - } - } - return dbs - } - itemID, _ := strconv.Atoi(cronjob.DBName) - if cronjob.DBType == "mysql" || cronjob.DBType == "mariadb" { - mysqlItem, _ := mysqlRepo.Get(commonRepo.WithByID(uint(itemID))) - dbs = append(dbs, databaseHelper{ - DBType: cronjob.DBType, - Database: mysqlItem.MysqlName, - Name: mysqlItem.Name, - }) - } else { - pgItem, _ := postgresqlRepo.Get(commonRepo.WithByID(uint(itemID))) - dbs = append(dbs, databaseHelper{ - DBType: cronjob.DBType, - Database: pgItem.PostgresqlName, - Name: pgItem.Name, - }) - } - return dbs -} - -func loadWebsForJob(cronjob model.Cronjob) []model.Website { - var weblist []model.Website - if cronjob.Website == "all" { - weblist, _ = websiteRepo.List() - return weblist - } - itemID, _ := strconv.Atoi(cronjob.Website) - webItem, _ := websiteRepo.GetFirst(commonRepo.WithByID(uint(itemID))) - if webItem.ID != 0 { - weblist = append(weblist, webItem) - } - return weblist -} - -func loadRecordPath(cronjob model.Cronjob, accountMap map[string]cronjobUploadHelper) (string, string) { - source := accountMap[fmt.Sprintf("%v", cronjob.DefaultDownload)].backType - targets := strings.Split(cronjob.BackupAccounts, ",") - var itemAccounts []string - for _, target := range targets { - if len(target) == 0 { - continue - } - if len(accountMap[target].backType) != 0 { - itemAccounts = append(itemAccounts, accountMap[target].backType) - } - } - backupType := strings.Join(itemAccounts, ",") - return source, backupType -} - -func handleBackupLogs(targetDir, fileName string, secret string) error { - websites, err := websiteRepo.List() - if err != nil { - return err - } - if len(websites) != 0 { - nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return err - } - webItem := path.Join(nginxInstall.GetPath(), "www/sites") - for _, website := range websites { - dirItem := path.Join(targetDir, "website", website.Alias) - if _, err := os.Stat(dirItem); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(dirItem, os.ModePerm); err != nil { - return err - } - } - itemDir := path.Join(webItem, website.Alias, "log") - logFiles, _ := os.ReadDir(itemDir) - if len(logFiles) != 0 { - for i := 0; i < len(logFiles); i++ { - if !logFiles[i].IsDir() { - _ = common.CopyFile(path.Join(itemDir, logFiles[i].Name()), dirItem) - } - } - } - itemDir2 := path.Join(global.CONF.System.Backup, "log/website", website.Alias) - logFiles2, _ := os.ReadDir(itemDir2) - if len(logFiles2) != 0 { - for i := 0; i < len(logFiles2); i++ { - if !logFiles2[i].IsDir() { - _ = common.CopyFile(path.Join(itemDir2, logFiles2[i].Name()), dirItem) - } - } - } - } - global.LOG.Debug("backup website log successful!") - } - - systemLogDir := path.Join(global.CONF.System.BaseDir, "1panel/log") - systemDir := path.Join(targetDir, "system") - if _, err := os.Stat(systemDir); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(systemDir, os.ModePerm); err != nil { - return err - } - } - systemLogFiles, _ := os.ReadDir(systemLogDir) - if len(systemLogFiles) != 0 { - for i := 0; i < len(systemLogFiles); i++ { - if !systemLogFiles[i].IsDir() { - _ = common.CopyFile(path.Join(systemLogDir, systemLogFiles[i].Name()), systemDir) - } - } - } - global.LOG.Debug("backup system log successful!") - - loginLogFiles, _ := os.ReadDir("/var/log") - loginDir := path.Join(targetDir, "login") - if _, err := os.Stat(loginDir); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(loginDir, os.ModePerm); err != nil { - return err - } - } - if len(loginLogFiles) != 0 { - for i := 0; i < len(loginLogFiles); i++ { - if !loginLogFiles[i].IsDir() && (strings.HasPrefix(loginLogFiles[i].Name(), "secure") || strings.HasPrefix(loginLogFiles[i].Name(), "auth.log")) { - _ = common.CopyFile(path.Join("/var/log", loginLogFiles[i].Name()), loginDir) - } - } - } - global.LOG.Debug("backup ssh log successful!") - - if err := handleTar(targetDir, path.Dir(targetDir), fileName, "", secret); err != nil { - return err - } - defer func() { - _ = os.RemoveAll(targetDir) - }() - return nil -} diff --git a/backend/app/service/cronjob_helper.go b/backend/app/service/cronjob_helper.go deleted file mode 100644 index aca743370..000000000 --- a/backend/app/service/cronjob_helper.go +++ /dev/null @@ -1,393 +0,0 @@ -package service - -import ( - "context" - "fmt" - "os" - "path" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/i18n" - - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/app/repo" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cloud_storage" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/1Panel-dev/1Panel/backend/utils/ntp" - "github.com/pkg/errors" -) - -func (u *CronjobService) HandleJob(cronjob *model.Cronjob) { - var ( - message []byte - err error - ) - record := cronjobRepo.StartRecords(cronjob.ID, cronjob.KeepLocal, "") - go func() { - switch cronjob.Type { - case "shell": - if len(cronjob.Script) == 0 { - return - } - record.Records = u.generateLogsPath(*cronjob, record.StartTime) - _ = cronjobRepo.UpdateRecords(record.ID, map[string]interface{}{"records": record.Records}) - script := cronjob.Script - if len(cronjob.ContainerName) != 0 { - command := "sh" - if len(cronjob.Command) != 0 { - command = cronjob.Command - } - script = fmt.Sprintf("docker exec %s %s -c \"%s\"", cronjob.ContainerName, command, strings.ReplaceAll(cronjob.Script, "\"", "\\\"")) - } - err = u.handleShell(cronjob.Type, cronjob.Name, script, record.Records) - u.removeExpiredLog(*cronjob) - case "curl": - if len(cronjob.URL) == 0 { - return - } - record.Records = u.generateLogsPath(*cronjob, record.StartTime) - _ = cronjobRepo.UpdateRecords(record.ID, map[string]interface{}{"records": record.Records}) - err = u.handleShell(cronjob.Type, cronjob.Name, fmt.Sprintf("curl '%s'", cronjob.URL), record.Records) - u.removeExpiredLog(*cronjob) - case "ntp": - err = u.handleNtpSync() - u.removeExpiredLog(*cronjob) - case "cutWebsiteLog": - var messageItem []string - messageItem, record.File, err = u.handleCutWebsiteLog(cronjob, record.StartTime) - message = []byte(strings.Join(messageItem, "\n")) - case "clean": - messageItem := "" - messageItem, err = u.handleSystemClean() - message = []byte(messageItem) - u.removeExpiredLog(*cronjob) - case "website": - err = u.handleWebsite(*cronjob, record.StartTime) - case "app": - err = u.handleApp(*cronjob, record.StartTime) - case "database": - err = u.handleDatabase(*cronjob, record.StartTime) - case "directory": - if len(cronjob.SourceDir) == 0 { - return - } - err = u.handleDirectory(*cronjob, record.StartTime) - case "log": - err = u.handleSystemLog(*cronjob, record.StartTime) - case "snapshot": - record.Records = u.generateLogsPath(*cronjob, record.StartTime) - _ = cronjobRepo.UpdateRecords(record.ID, map[string]interface{}{"records": record.Records}) - err = u.handleSnapshot(*cronjob, record.StartTime, record.Records) - } - - if err != nil { - if len(message) != 0 { - record.Records, _ = mkdirAndWriteFile(cronjob, record.StartTime, message) - } - cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records) - return - } - if len(message) != 0 { - record.Records, err = mkdirAndWriteFile(cronjob, record.StartTime, message) - if err != nil { - global.LOG.Errorf("save file %s failed, err: %v", record.Records, err) - } - } - cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records) - }() -} - -func (u *CronjobService) handleShell(cronType, cornName, script, logPath string) error { - handleDir := fmt.Sprintf("%s/task/%s/%s", constant.DataDir, cronType, cornName) - if _, err := os.Stat(handleDir); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(handleDir, os.ModePerm); err != nil { - return err - } - } - if err := cmd.ExecCronjobWithTimeOut(script, handleDir, logPath, 24*time.Hour); err != nil { - return err - } - return nil -} - -func (u *CronjobService) handleNtpSync() error { - ntpServer, err := settingRepo.Get(settingRepo.WithByKey("NtpSite")) - if err != nil { - return err - } - ntime, err := ntp.GetRemoteTime(ntpServer.Value) - if err != nil { - return err - } - if err := ntp.UpdateSystemTime(ntime.Format(constant.DateTimeLayout)); err != nil { - return err - } - return nil -} - -func handleTar(sourceDir, targetDir, name, exclusionRules string, secret string) error { - if _, err := os.Stat(targetDir); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(targetDir, os.ModePerm); err != nil { - return err - } - } - - excludes := strings.Split(exclusionRules, ",") - excludeRules := "" - excludes = append(excludes, "*.sock") - for _, exclude := range excludes { - if len(exclude) == 0 { - continue - } - excludeRules += " --exclude " + exclude - } - path := "" - if strings.Contains(sourceDir, "/") { - itemDir := strings.ReplaceAll(sourceDir[strings.LastIndex(sourceDir, "/"):], "/", "") - aheadDir := sourceDir[:strings.LastIndex(sourceDir, "/")] - if len(aheadDir) == 0 { - aheadDir = "/" - } - path += fmt.Sprintf("-C %s %s", aheadDir, itemDir) - } else { - path = sourceDir - } - - commands := "" - - if len(secret) != 0 { - extraCmd := "| openssl enc -aes-256-cbc -salt -k '" + secret + "' -out" - commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read -zcf %s %s %s %s", " -"+excludeRules, path, extraCmd, targetDir+"/"+name) - global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******")) - } else { - commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read -zcf %s %s %s", targetDir+"/"+name, excludeRules, path) - global.LOG.Debug(commands) - } - stdout, err := cmd.ExecWithTimeOut(commands, 24*time.Hour) - if err != nil { - if len(stdout) != 0 { - global.LOG.Errorf("do handle tar failed, stdout: %s, err: %v", stdout, err) - return fmt.Errorf("do handle tar failed, stdout: %s, err: %v", stdout, err) - } - } - return nil -} - -func handleUnTar(sourceFile, targetDir string, secret string) error { - if _, err := os.Stat(targetDir); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(targetDir, os.ModePerm); err != nil { - return err - } - } - commands := "" - if len(secret) != 0 { - extraCmd := "openssl enc -d -aes-256-cbc -k '" + secret + "' -in " + sourceFile + " | " - commands = fmt.Sprintf("%s tar -zxvf - -C %s", extraCmd, targetDir+" > /dev/null 2>&1") - global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******")) - } else { - commands = fmt.Sprintf("tar zxvfC %s %s", sourceFile, targetDir) - global.LOG.Debug(commands) - } - - stdout, err := cmd.ExecWithTimeOut(commands, 24*time.Hour) - if err != nil { - global.LOG.Errorf("do handle untar failed, stdout: %s, err: %v", stdout, err) - return errors.New(stdout) - } - return nil -} - -func (u *CronjobService) handleCutWebsiteLog(cronjob *model.Cronjob, startTime time.Time) ([]string, string, error) { - var ( - err error - filePaths []string - msgs []string - ) - websites := loadWebsForJob(*cronjob) - nginx, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return msgs, "", nil - } - baseDir := path.Join(nginx.GetPath(), "www", "sites") - fileOp := files.NewFileOp() - for _, website := range websites { - websiteLogDir := path.Join(baseDir, website.Alias, "log") - srcAccessLogPath := path.Join(websiteLogDir, "access.log") - srcErrorLogPath := path.Join(websiteLogDir, "error.log") - dstLogDir := path.Join(global.CONF.System.Backup, "log", "website", website.Alias) - if !fileOp.Stat(dstLogDir) { - _ = os.MkdirAll(dstLogDir, 0755) - } - - dstName := fmt.Sprintf("%s_log_%s.gz", website.PrimaryDomain, startTime.Format(constant.DateTimeSlimLayout)) - dstFilePath := path.Join(dstLogDir, dstName) - filePaths = append(filePaths, dstFilePath) - - if err = backupLogFile(dstFilePath, websiteLogDir, fileOp); err != nil { - websiteErr := buserr.WithNameAndErr("ErrCutWebsiteLog", website.PrimaryDomain, err) - err = websiteErr - msgs = append(msgs, websiteErr.Error()) - global.LOG.Error(websiteErr.Error()) - continue - } else { - _ = fileOp.WriteFile(srcAccessLogPath, strings.NewReader(""), 0755) - _ = fileOp.WriteFile(srcErrorLogPath, strings.NewReader(""), 0755) - } - msg := i18n.GetMsgWithMap("CutWebsiteLogSuccess", map[string]interface{}{"name": website.PrimaryDomain, "path": dstFilePath}) - global.LOG.Infof(msg) - msgs = append(msgs, msg) - } - u.removeExpiredLog(*cronjob) - return msgs, strings.Join(filePaths, ","), err -} - -func backupLogFile(dstFilePath, websiteLogDir string, fileOp files.FileOp) error { - if err := cmd.ExecCmd(fmt.Sprintf("tar -czf %s -C %s %s", dstFilePath, websiteLogDir, strings.Join([]string{"access.log", "error.log"}, " "))); err != nil { - dstDir := path.Dir(dstFilePath) - if err = fileOp.Copy(path.Join(websiteLogDir, "access.log"), dstDir); err != nil { - return err - } - if err = fileOp.Copy(path.Join(websiteLogDir, "error.log"), dstDir); err != nil { - return err - } - if err = cmd.ExecCmd(fmt.Sprintf("tar -czf %s -C %s %s", dstFilePath, dstDir, strings.Join([]string{"access.log", "error.log"}, " "))); err != nil { - return err - } - _ = fileOp.DeleteFile(path.Join(dstDir, "access.log")) - _ = fileOp.DeleteFile(path.Join(dstDir, "error.log")) - return nil - } - return nil -} - -func (u *CronjobService) handleSystemClean() (string, error) { - return NewIDeviceService().CleanForCronjob() -} - -func loadClientMap(backupAccounts string) (map[string]cronjobUploadHelper, error) { - clients := make(map[string]cronjobUploadHelper) - accounts, err := backupRepo.List() - if err != nil { - return nil, err - } - targets := strings.Split(backupAccounts, ",") - for _, target := range targets { - if len(target) == 0 { - continue - } - for _, account := range accounts { - if target == account.Type { - client, err := NewIBackupService().NewClient(&account) - if err != nil { - return nil, err - } - pathItem := account.BackupPath - if account.BackupPath != "/" { - pathItem = strings.TrimPrefix(account.BackupPath, "/") - } - clients[target] = cronjobUploadHelper{ - client: client, - backupPath: pathItem, - backType: account.Type, - } - } - } - } - return clients, nil -} - -type cronjobUploadHelper struct { - backupPath string - backType string - client cloud_storage.CloudStorageClient -} - -func (u *CronjobService) uploadCronjobBackFile(cronjob model.Cronjob, accountMap map[string]cronjobUploadHelper, file string) (string, error) { - defer func() { - _ = os.Remove(file) - }() - accounts := strings.Split(cronjob.BackupAccounts, ",") - cloudSrc := strings.TrimPrefix(file, global.CONF.System.TmpDir+"/") - for _, account := range accounts { - if len(account) != 0 { - global.LOG.Debugf("start upload file to %s, dir: %s", account, path.Join(accountMap[account].backupPath, cloudSrc)) - if _, err := accountMap[account].client.Upload(file, path.Join(accountMap[account].backupPath, cloudSrc)); err != nil { - return "", err - } - global.LOG.Debugf("upload successful!") - } - } - return cloudSrc, nil -} - -func (u *CronjobService) removeExpiredBackup(cronjob model.Cronjob, accountMap map[string]cronjobUploadHelper, record model.BackupRecord) { - global.LOG.Infof("start to handle remove expired, retain copies: %d", cronjob.RetainCopies) - var opts []repo.DBOption - opts = append(opts, commonRepo.WithByFrom("cronjob")) - opts = append(opts, backupRepo.WithByCronID(cronjob.ID)) - opts = append(opts, commonRepo.WithOrderBy("created_at desc")) - if record.ID != 0 { - opts = append(opts, backupRepo.WithByType(record.Type)) - opts = append(opts, commonRepo.WithByName(record.Name)) - opts = append(opts, backupRepo.WithByDetailName(record.DetailName)) - } - records, _ := backupRepo.ListRecord(opts...) - if len(records) <= int(cronjob.RetainCopies) { - return - } - for i := int(cronjob.RetainCopies); i < len(records); i++ { - accounts := strings.Split(cronjob.BackupAccounts, ",") - if cronjob.Type == "snapshot" { - for _, account := range accounts { - if len(account) != 0 { - _, _ = accountMap[account].client.Delete(path.Join(accountMap[account].backupPath, "system_snapshot", records[i].FileName)) - } - } - _ = snapshotRepo.Delete(commonRepo.WithByName(strings.TrimSuffix(records[i].FileName, ".tar.gz"))) - } else { - for _, account := range accounts { - if len(account) != 0 { - _, _ = accountMap[account].client.Delete(path.Join(accountMap[account].backupPath, records[i].FileDir, records[i].FileName)) - } - } - } - _ = backupRepo.DeleteRecord(context.Background(), commonRepo.WithByID(records[i].ID)) - } -} - -func (u *CronjobService) removeExpiredLog(cronjob model.Cronjob) { - global.LOG.Infof("start to handle remove expired, retain copies: %d", cronjob.RetainCopies) - records, _ := cronjobRepo.ListRecord(cronjobRepo.WithByJobID(int(cronjob.ID)), commonRepo.WithOrderBy("created_at desc")) - if len(records) <= int(cronjob.RetainCopies) { - return - } - for i := int(cronjob.RetainCopies); i < len(records); i++ { - if len(records[i].File) != 0 { - files := strings.Split(records[i].File, ",") - for _, file := range files { - _ = os.Remove(file) - } - } - _ = cronjobRepo.DeleteRecord(commonRepo.WithByID(uint(records[i].ID))) - _ = os.Remove(records[i].Records) - } -} - -func (u *CronjobService) generateLogsPath(cronjob model.Cronjob, startTime time.Time) string { - dir := fmt.Sprintf("%s/task/%s/%s", constant.DataDir, cronjob.Type, cronjob.Name) - if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) { - _ = os.MkdirAll(dir, os.ModePerm) - } - - path := fmt.Sprintf("%s/%s.log", dir, startTime.Format(constant.DateTimeSlimLayout)) - return path -} - -func hasBackup(cronjobType string) bool { - return cronjobType == "app" || cronjobType == "database" || cronjobType == "website" || cronjobType == "directory" || cronjobType == "snapshot" || cronjobType == "log" -} diff --git a/backend/app/service/dashboard.go b/backend/app/service/dashboard.go deleted file mode 100644 index 0ac4af844..000000000 --- a/backend/app/service/dashboard.go +++ /dev/null @@ -1,325 +0,0 @@ -package service - -import ( - "encoding/json" - "fmt" - "sort" - "strings" - "sync" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/copier" - "github.com/1Panel-dev/1Panel/backend/utils/xpack" - "github.com/shirou/gopsutil/v3/cpu" - "github.com/shirou/gopsutil/v3/disk" - "github.com/shirou/gopsutil/v3/host" - "github.com/shirou/gopsutil/v3/load" - "github.com/shirou/gopsutil/v3/mem" - "github.com/shirou/gopsutil/v3/net" -) - -type DashboardService struct{} - -type IDashboardService interface { - LoadOsInfo() (*dto.OsInfo, error) - LoadBaseInfo(ioOption string, netOption string) (*dto.DashboardBase, error) - LoadCurrentInfo(ioOption string, netOption string) *dto.DashboardCurrent - - Restart(operation string) error -} - -func NewIDashboardService() IDashboardService { - return &DashboardService{} -} - -func (u *DashboardService) Restart(operation string) error { - if operation != "1panel" && operation != "system" { - return fmt.Errorf("handle restart operation %s failed, err: nonsupport such operation", operation) - } - itemCmd := fmt.Sprintf("%s 1pctl restart", cmd.SudoHandleCmd()) - if operation == "system" { - itemCmd = fmt.Sprintf("%s reboot", cmd.SudoHandleCmd()) - } - go func() { - stdout, err := cmd.Exec(itemCmd) - if err != nil { - global.LOG.Errorf("handle %s failed, err: %v", itemCmd, stdout) - } - }() - return nil -} - -func (u *DashboardService) LoadOsInfo() (*dto.OsInfo, error) { - var baseInfo dto.OsInfo - hostInfo, err := host.Info() - if err != nil { - return nil, err - } - baseInfo.OS = hostInfo.OS - baseInfo.Platform = hostInfo.Platform - baseInfo.PlatformFamily = hostInfo.PlatformFamily - baseInfo.KernelArch = hostInfo.KernelArch - baseInfo.KernelVersion = hostInfo.KernelVersion - - diskInfo, err := disk.Usage(global.CONF.System.BaseDir) - if err == nil { - baseInfo.DiskSize = int64(diskInfo.Free) - } - - if baseInfo.KernelArch == "armv7l" { - baseInfo.KernelArch = "armv7" - } - if baseInfo.KernelArch == "x86_64" { - baseInfo.KernelArch = "amd64" - } - return &baseInfo, nil -} - -func (u *DashboardService) LoadBaseInfo(ioOption string, netOption string) (*dto.DashboardBase, error) { - var baseInfo dto.DashboardBase - hostInfo, err := host.Info() - if err != nil { - return nil, err - } - baseInfo.Hostname = hostInfo.Hostname - baseInfo.OS = hostInfo.OS - baseInfo.Platform = hostInfo.Platform - baseInfo.PlatformFamily = hostInfo.PlatformFamily - baseInfo.PlatformVersion = hostInfo.PlatformVersion - baseInfo.KernelArch = hostInfo.KernelArch - baseInfo.KernelVersion = hostInfo.KernelVersion - ss, _ := json.Marshal(hostInfo) - baseInfo.VirtualizationSystem = string(ss) - - appInstall, err := appInstallRepo.ListBy() - if err != nil { - return nil, err - } - baseInfo.AppInstalledNumber = len(appInstall) - postgresqlDbs, err := postgresqlRepo.List() - if err != nil { - return nil, err - } - mysqlDbs, err := mysqlRepo.List() - if err != nil { - return nil, err - } - baseInfo.DatabaseNumber = len(mysqlDbs) + len(postgresqlDbs) - website, err := websiteRepo.GetBy() - if err != nil { - return nil, err - } - baseInfo.WebsiteNumber = len(website) - cronjobs, err := cronjobRepo.List() - if err != nil { - return nil, err - } - baseInfo.CronjobNumber = len(cronjobs) - - cpuInfo, err := cpu.Info() - if err == nil { - baseInfo.CPUModelName = cpuInfo[0].ModelName - } - - baseInfo.CPUCores, _ = cpu.Counts(false) - baseInfo.CPULogicalCores, _ = cpu.Counts(true) - - baseInfo.CurrentInfo = *u.LoadCurrentInfo(ioOption, netOption) - return &baseInfo, nil -} - -func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *dto.DashboardCurrent { - var currentInfo dto.DashboardCurrent - hostInfo, _ := host.Info() - currentInfo.Uptime = hostInfo.Uptime - currentInfo.TimeSinceUptime = time.Now().Add(-time.Duration(hostInfo.Uptime) * time.Second).Format(constant.DateTimeLayout) - currentInfo.Procs = hostInfo.Procs - - currentInfo.CPUTotal, _ = cpu.Counts(true) - totalPercent, _ := cpu.Percent(0, false) - if len(totalPercent) == 1 { - currentInfo.CPUUsedPercent = totalPercent[0] - currentInfo.CPUUsed = currentInfo.CPUUsedPercent * 0.01 * float64(currentInfo.CPUTotal) - } - currentInfo.CPUPercent, _ = cpu.Percent(0, true) - - loadInfo, _ := load.Avg() - currentInfo.Load1 = loadInfo.Load1 - currentInfo.Load5 = loadInfo.Load5 - currentInfo.Load15 = loadInfo.Load15 - currentInfo.LoadUsagePercent = loadInfo.Load1 / (float64(currentInfo.CPUTotal*2) * 0.75) * 100 - - memoryInfo, _ := mem.VirtualMemory() - currentInfo.MemoryTotal = memoryInfo.Total - currentInfo.MemoryAvailable = memoryInfo.Available - currentInfo.MemoryUsed = memoryInfo.Used - currentInfo.MemoryUsedPercent = memoryInfo.UsedPercent - - swapInfo, _ := mem.SwapMemory() - currentInfo.SwapMemoryTotal = swapInfo.Total - currentInfo.SwapMemoryAvailable = swapInfo.Free - currentInfo.SwapMemoryUsed = swapInfo.Used - currentInfo.SwapMemoryUsedPercent = swapInfo.UsedPercent - - currentInfo.DiskData = loadDiskInfo() - currentInfo.GPUData = loadGPUInfo() - - if ioOption == "all" { - diskInfo, _ := disk.IOCounters() - for _, state := range diskInfo { - currentInfo.IOReadBytes += state.ReadBytes - currentInfo.IOWriteBytes += state.WriteBytes - currentInfo.IOCount += (state.ReadCount + state.WriteCount) - currentInfo.IOReadTime += state.ReadTime - currentInfo.IOWriteTime += state.WriteTime - } - } else { - diskInfo, _ := disk.IOCounters(ioOption) - for _, state := range diskInfo { - currentInfo.IOReadBytes += state.ReadBytes - currentInfo.IOWriteBytes += state.WriteBytes - currentInfo.IOCount += (state.ReadCount + state.WriteCount) - currentInfo.IOReadTime += state.ReadTime - currentInfo.IOWriteTime += state.WriteTime - } - } - - if netOption == "all" { - netInfo, _ := net.IOCounters(false) - if len(netInfo) != 0 { - currentInfo.NetBytesSent = netInfo[0].BytesSent - currentInfo.NetBytesRecv = netInfo[0].BytesRecv - } - } else { - netInfo, _ := net.IOCounters(true) - for _, state := range netInfo { - if state.Name == netOption { - currentInfo.NetBytesSent = state.BytesSent - currentInfo.NetBytesRecv = state.BytesRecv - } - } - } - - currentInfo.ShotTime = time.Now() - return ¤tInfo -} - -type diskInfo struct { - Type string - Mount string - Device string -} - -func loadDiskInfo() []dto.DiskInfo { - var datas []dto.DiskInfo - stdout, err := cmd.ExecWithTimeOut("df -hT -P|grep '/'|grep -v tmpfs|grep -v 'snap/core'|grep -v udev", 2*time.Second) - if err != nil { - stdout, err = cmd.ExecWithTimeOut("df -lhT -P|grep '/'|grep -v tmpfs|grep -v 'snap/core'|grep -v udev", 1*time.Second) - if err != nil { - return datas - } - } - lines := strings.Split(stdout, "\n") - - var mounts []diskInfo - var excludes = []string{"/mnt/cdrom", "/boot", "/boot/efi", "/dev", "/dev/shm", "/run/lock", "/run", "/run/shm", "/run/user"} - for _, line := range lines { - fields := strings.Fields(line) - if len(fields) < 7 { - continue - } - if strings.HasPrefix(fields[6], "/snap") || len(strings.Split(fields[6], "/")) > 10 { - continue - } - if strings.TrimSpace(fields[1]) == "tmpfs" { - continue - } - if strings.Contains(fields[2], "K") { - continue - } - if strings.Contains(fields[6], "docker") { - continue - } - isExclude := false - for _, exclude := range excludes { - if exclude == fields[6] { - isExclude = true - } - } - if isExclude { - continue - } - mounts = append(mounts, diskInfo{Type: fields[1], Device: fields[0], Mount: strings.Join(fields[6:], " ")}) - } - - var ( - wg sync.WaitGroup - mu sync.Mutex - ) - wg.Add(len(mounts)) - for i := 0; i < len(mounts); i++ { - go func(timeoutCh <-chan time.Time, mount diskInfo) { - defer wg.Done() - - var itemData dto.DiskInfo - itemData.Path = mount.Mount - itemData.Type = mount.Type - itemData.Device = mount.Device - select { - case <-timeoutCh: - mu.Lock() - datas = append(datas, itemData) - mu.Unlock() - global.LOG.Errorf("load disk info from %s failed, err: timeout", mount.Mount) - default: - state, err := disk.Usage(mount.Mount) - if err != nil { - mu.Lock() - datas = append(datas, itemData) - mu.Unlock() - global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, err) - return - } - itemData.Total = state.Total - itemData.Free = state.Free - itemData.Used = state.Used - itemData.UsedPercent = state.UsedPercent - itemData.InodesTotal = state.InodesTotal - itemData.InodesUsed = state.InodesUsed - itemData.InodesFree = state.InodesFree - itemData.InodesUsedPercent = state.InodesUsedPercent - mu.Lock() - datas = append(datas, itemData) - mu.Unlock() - } - }(time.After(5*time.Second), mounts[i]) - } - wg.Wait() - - sort.Slice(datas, func(i, j int) bool { - return datas[i].Path < datas[j].Path - }) - return datas -} - -func loadGPUInfo() []dto.GPUInfo { - list := xpack.LoadGpuInfo() - if len(list) == 0 { - return nil - } - var data []dto.GPUInfo - for _, gpu := range list { - var dataItem dto.GPUInfo - if err := copier.Copy(&dataItem, &gpu); err != nil { - continue - } - dataItem.PowerUsage = dataItem.PowerDraw + " / " + dataItem.MaxPowerLimit - dataItem.MemoryUsage = dataItem.MemUsed + " / " + dataItem.MemTotal - data = append(data, dataItem) - } - return data -} diff --git a/backend/app/service/database.go b/backend/app/service/database.go deleted file mode 100644 index 6804d4c71..000000000 --- a/backend/app/service/database.go +++ /dev/null @@ -1,330 +0,0 @@ -package service - -import ( - "context" - "fmt" - "os" - "path" - - "github.com/1Panel-dev/1Panel/backend/utils/postgresql" - pgclient "github.com/1Panel-dev/1Panel/backend/utils/postgresql/client" - redisclient "github.com/1Panel-dev/1Panel/backend/utils/redis" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/encrypt" - "github.com/1Panel-dev/1Panel/backend/utils/mysql" - "github.com/1Panel-dev/1Panel/backend/utils/mysql/client" - "github.com/jinzhu/copier" - "github.com/pkg/errors" -) - -type DatabaseService struct{} - -type IDatabaseService interface { - Get(name string) (dto.DatabaseInfo, error) - SearchWithPage(search dto.DatabaseSearch) (int64, interface{}, error) - CheckDatabase(req dto.DatabaseCreate) bool - Create(req dto.DatabaseCreate) error - Update(req dto.DatabaseUpdate) error - DeleteCheck(id uint) ([]string, error) - Delete(req dto.DatabaseDelete) error - List(dbType string) ([]dto.DatabaseOption, error) - LoadItems(dbType string) ([]dto.DatabaseItem, error) -} - -func NewIDatabaseService() IDatabaseService { - return &DatabaseService{} -} - -func (u *DatabaseService) SearchWithPage(search dto.DatabaseSearch) (int64, interface{}, error) { - total, dbs, err := databaseRepo.Page(search.Page, search.PageSize, - databaseRepo.WithTypeList(search.Type), - commonRepo.WithLikeName(search.Info), - commonRepo.WithOrderRuleBy(search.OrderBy, search.Order), - databaseRepo.WithoutByFrom("local"), - ) - var datas []dto.DatabaseInfo - for _, db := range dbs { - var item dto.DatabaseInfo - if err := copier.Copy(&item, &db); err != nil { - return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - datas = append(datas, item) - } - return total, datas, err -} - -func (u *DatabaseService) Get(name string) (dto.DatabaseInfo, error) { - var data dto.DatabaseInfo - remote, err := databaseRepo.Get(commonRepo.WithByName(name)) - if err != nil { - return data, err - } - if err := copier.Copy(&data, &remote); err != nil { - return data, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - return data, nil -} - -func (u *DatabaseService) List(dbType string) ([]dto.DatabaseOption, error) { - dbs, err := databaseRepo.GetList(databaseRepo.WithTypeList(dbType)) - if err != nil { - return nil, err - } - var datas []dto.DatabaseOption - for _, db := range dbs { - var item dto.DatabaseOption - if err := copier.Copy(&item, &db); err != nil { - return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - item.Database = db.Name - datas = append(datas, item) - } - return datas, err -} - -func (u *DatabaseService) LoadItems(dbType string) ([]dto.DatabaseItem, error) { - dbs, err := databaseRepo.GetList(databaseRepo.WithTypeList(dbType)) - var datas []dto.DatabaseItem - for _, db := range dbs { - if dbType == "postgresql" { - items, _ := postgresqlRepo.List(postgresqlRepo.WithByPostgresqlName(db.Name)) - for _, item := range items { - var dItem dto.DatabaseItem - if err := copier.Copy(&dItem, &item); err != nil { - continue - } - dItem.Database = db.Name - datas = append(datas, dItem) - } - } else { - items, _ := mysqlRepo.List(mysqlRepo.WithByMysqlName(db.Name)) - for _, item := range items { - var dItem dto.DatabaseItem - if err := copier.Copy(&dItem, &item); err != nil { - continue - } - dItem.Database = db.Name - datas = append(datas, dItem) - } - } - } - return datas, err -} - -func (u *DatabaseService) CheckDatabase(req dto.DatabaseCreate) bool { - switch req.Type { - case constant.AppPostgresql: - _, err := postgresql.NewPostgresqlClient(pgclient.DBInfo{ - From: "remote", - Address: req.Address, - Port: req.Port, - Username: req.Username, - Password: req.Password, - Timeout: 6, - }) - return err == nil - case constant.AppRedis: - _, err := redisclient.NewRedisClient(redisclient.DBInfo{ - Address: req.Address, - Port: req.Port, - Password: req.Password, - }) - return err == nil - case "mysql", "mariadb": - _, err := mysql.NewMysqlClient(client.DBInfo{ - From: "remote", - Address: req.Address, - Port: req.Port, - Username: req.Username, - Password: req.Password, - - SSL: req.SSL, - RootCert: req.RootCert, - ClientKey: req.ClientKey, - ClientCert: req.ClientCert, - SkipVerify: req.SkipVerify, - Timeout: 6, - }) - return err == nil - } - - return false -} - -func (u *DatabaseService) Create(req dto.DatabaseCreate) error { - db, _ := databaseRepo.Get(commonRepo.WithByName(req.Name)) - if db.ID != 0 { - if db.From == "local" { - return buserr.New(constant.ErrLocalExist) - } - return constant.ErrRecordExist - } - switch req.Type { - case constant.AppPostgresql: - if _, err := postgresql.NewPostgresqlClient(pgclient.DBInfo{ - From: "remote", - Address: req.Address, - Port: req.Port, - Username: req.Username, - Password: req.Password, - Timeout: 6, - }); err != nil { - return err - } - case constant.AppRedis: - if _, err := redisclient.NewRedisClient(redisclient.DBInfo{ - Address: req.Address, - Port: req.Port, - Password: req.Password, - }); err != nil { - return err - } - case "mysql", "mariadb": - if _, err := mysql.NewMysqlClient(client.DBInfo{ - From: "remote", - Address: req.Address, - Port: req.Port, - Username: req.Username, - Password: req.Password, - - SSL: req.SSL, - RootCert: req.RootCert, - ClientKey: req.ClientKey, - ClientCert: req.ClientCert, - SkipVerify: req.SkipVerify, - Timeout: 6, - }); err != nil { - return err - } - default: - return errors.New("database type not supported") - } - - if err := copier.Copy(&db, &req); err != nil { - return errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - if err := databaseRepo.Create(context.Background(), &db); err != nil { - return err - } - return nil -} - -func (u *DatabaseService) DeleteCheck(id uint) ([]string, error) { - var appInUsed []string - apps, _ := appInstallResourceRepo.GetBy(databaseRepo.WithByFrom("remote"), appInstallResourceRepo.WithLinkId(id)) - for _, app := range apps { - appInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(app.AppInstallId)) - if appInstall.ID != 0 { - appInUsed = append(appInUsed, appInstall.Name) - } - } - - return appInUsed, nil -} - -func (u *DatabaseService) Delete(req dto.DatabaseDelete) error { - db, _ := databaseRepo.Get(commonRepo.WithByID(req.ID)) - if db.ID == 0 { - return constant.ErrRecordNotFound - } - - if req.DeleteBackup { - uploadDir := path.Join(global.CONF.System.BaseDir, fmt.Sprintf("1panel/uploads/database/%s/%s", db.Type, db.Name)) - if _, err := os.Stat(uploadDir); err == nil { - _ = os.RemoveAll(uploadDir) - } - localDir, err := loadLocalDir() - if err != nil && !req.ForceDelete { - return err - } - backupDir := path.Join(localDir, fmt.Sprintf("database/%s/%s", db.Type, db.Name)) - if _, err := os.Stat(backupDir); err == nil { - _ = os.RemoveAll(backupDir) - } - _ = backupRepo.DeleteRecord(context.Background(), commonRepo.WithByType(db.Type), commonRepo.WithByName(db.Name)) - global.LOG.Infof("delete database %s-%s backups successful", db.Type, db.Name) - } - - if err := databaseRepo.Delete(context.Background(), commonRepo.WithByID(req.ID)); err != nil && !req.ForceDelete { - return err - } - if db.From != "local" { - if db.Type == "mysql" || db.Type == "mariadb" { - if err := mysqlRepo.Delete(context.Background(), mysqlRepo.WithByMysqlName(db.Name)); err != nil && !req.ForceDelete { - return err - } - } else { - if err := postgresqlRepo.Delete(context.Background(), postgresqlRepo.WithByPostgresqlName(db.Name)); err != nil && !req.ForceDelete { - return err - } - } - } - return nil -} - -func (u *DatabaseService) Update(req dto.DatabaseUpdate) error { - switch req.Type { - case constant.AppPostgresql: - if _, err := postgresql.NewPostgresqlClient(pgclient.DBInfo{ - From: "remote", - Address: req.Address, - Port: req.Port, - Username: req.Username, - Password: req.Password, - Timeout: 300, - }); err != nil { - return err - } - case constant.AppRedis: - if _, err := redisclient.NewRedisClient(redisclient.DBInfo{ - Address: req.Address, - Port: req.Port, - Password: req.Password, - }); err != nil { - return err - } - case "mysql", "mariadb": - if _, err := mysql.NewMysqlClient(client.DBInfo{ - From: "remote", - Address: req.Address, - Port: req.Port, - Username: req.Username, - Password: req.Password, - - SSL: req.SSL, - RootCert: req.RootCert, - ClientKey: req.ClientKey, - ClientCert: req.ClientCert, - SkipVerify: req.SkipVerify, - Timeout: 300, - }); err != nil { - return err - } - default: - return errors.New("database type not supported") - } - - pass, err := encrypt.StringEncrypt(req.Password) - if err != nil { - return fmt.Errorf("decrypt database password failed, err: %v", err) - } - - upMap := make(map[string]interface{}) - upMap["type"] = req.Type - upMap["version"] = req.Version - upMap["address"] = req.Address - upMap["port"] = req.Port - upMap["username"] = req.Username - upMap["password"] = pass - upMap["description"] = req.Description - upMap["ssl"] = req.SSL - upMap["client_key"] = req.ClientKey - upMap["client_cert"] = req.ClientCert - upMap["root_cert"] = req.RootCert - upMap["skip_verify"] = req.SkipVerify - return databaseRepo.Update(req.ID, upMap) -} diff --git a/backend/app/service/database_common.go b/backend/app/service/database_common.go deleted file mode 100644 index 75d6973e6..000000000 --- a/backend/app/service/database_common.go +++ /dev/null @@ -1,94 +0,0 @@ -package service - -import ( - "bufio" - "fmt" - "os" - "path" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/compose" -) - -type DBCommonService struct{} - -type IDBCommonService interface { - LoadBaseInfo(req dto.OperationWithNameAndType) (*dto.DBBaseInfo, error) - LoadDatabaseFile(req dto.OperationWithNameAndType) (string, error) - - UpdateConfByFile(req dto.DBConfUpdateByFile) error -} - -func NewIDBCommonService() IDBCommonService { - return &DBCommonService{} -} - -func (u *DBCommonService) LoadBaseInfo(req dto.OperationWithNameAndType) (*dto.DBBaseInfo, error) { - var data dto.DBBaseInfo - app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name) - if err != nil { - return nil, err - } - data.ContainerName = app.ContainerName - data.Name = app.Name - data.Port = int64(app.Port) - - return &data, nil -} - -func (u *DBCommonService) LoadDatabaseFile(req dto.OperationWithNameAndType) (string, error) { - filePath := "" - switch req.Type { - case "mysql-conf": - filePath = path.Join(global.CONF.System.DataDir, fmt.Sprintf("apps/mysql/%s/conf/my.cnf", req.Name)) - case "mariadb-conf": - filePath = path.Join(global.CONF.System.DataDir, fmt.Sprintf("apps/mariadb/%s/conf/my.cnf", req.Name)) - case "postgresql-conf": - filePath = path.Join(global.CONF.System.DataDir, fmt.Sprintf("apps/postgresql/%s/data/postgresql.conf", req.Name)) - case "redis-conf": - filePath = path.Join(global.CONF.System.DataDir, fmt.Sprintf("apps/redis/%s/conf/redis.conf", req.Name)) - case "mysql-slow-logs": - filePath = path.Join(global.CONF.System.DataDir, fmt.Sprintf("apps/mysql/%s/data/1Panel-slow.log", req.Name)) - case "mariadb-slow-logs": - filePath = path.Join(global.CONF.System.DataDir, fmt.Sprintf("apps/mariadb/%s/db/data/1Panel-slow.log", req.Name)) - } - if _, err := os.Stat(filePath); err != nil { - return "", buserr.New("ErrHttpReqNotFound") - } - content, err := os.ReadFile(filePath) - if err != nil { - return "", err - } - return string(content), nil -} - -func (u *DBCommonService) UpdateConfByFile(req dto.DBConfUpdateByFile) error { - app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Database) - if err != nil { - return err - } - path := "" - switch req.Type { - case constant.AppMariaDB, constant.AppMysql: - path = fmt.Sprintf("%s/%s/%s/conf/my.cnf", constant.AppInstallDir, req.Type, app.Name) - case constant.AppPostgresql: - path = fmt.Sprintf("%s/%s/%s/data/postgresql.conf", constant.AppInstallDir, req.Type, app.Name) - case constant.AppRedis: - path = fmt.Sprintf("%s/%s/%s/conf/redis.conf", constant.AppInstallDir, req.Type, app.Name) - } - file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640) - if err != nil { - return err - } - defer file.Close() - write := bufio.NewWriter(file) - _, _ = write.WriteString(req.File) - write.Flush() - if _, err := compose.Restart(fmt.Sprintf("%s/%s/%s/docker-compose.yml", constant.AppInstallDir, req.Type, app.Name)); err != nil { - return err - } - return nil -} diff --git a/backend/app/service/database_mysql.go b/backend/app/service/database_mysql.go deleted file mode 100644 index 07b39b50e..000000000 --- a/backend/app/service/database_mysql.go +++ /dev/null @@ -1,662 +0,0 @@ -package service - -import ( - "context" - "encoding/json" - "fmt" - "os" - "os/exec" - "path" - "regexp" - "strconv" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/1Panel-dev/1Panel/backend/utils/compose" - "github.com/1Panel-dev/1Panel/backend/utils/encrypt" - "github.com/1Panel-dev/1Panel/backend/utils/mysql" - "github.com/1Panel-dev/1Panel/backend/utils/mysql/client" - _ "github.com/go-sql-driver/mysql" - "github.com/jinzhu/copier" - "github.com/pkg/errors" -) - -type MysqlService struct{} - -type IMysqlService interface { - SearchWithPage(search dto.MysqlDBSearch) (int64, interface{}, error) - ListDBOption() ([]dto.MysqlOption, error) - Create(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error) - BindUser(req dto.BindUser) error - LoadFromRemote(req dto.MysqlLoadDB) error - ChangeAccess(info dto.ChangeDBInfo) error - ChangePassword(info dto.ChangeDBInfo) error - UpdateVariables(req dto.MysqlVariablesUpdate) error - UpdateDescription(req dto.UpdateDescription) error - DeleteCheck(req dto.MysqlDBDeleteCheck) ([]string, error) - Delete(ctx context.Context, req dto.MysqlDBDelete) error - - LoadStatus(req dto.OperationWithNameAndType) (*dto.MysqlStatus, error) - LoadVariables(req dto.OperationWithNameAndType) (*dto.MysqlVariables, error) - LoadRemoteAccess(req dto.OperationWithNameAndType) (bool, error) -} - -func NewIMysqlService() IMysqlService { - return &MysqlService{} -} - -func (u *MysqlService) SearchWithPage(search dto.MysqlDBSearch) (int64, interface{}, error) { - total, mysqls, err := mysqlRepo.Page(search.Page, search.PageSize, - mysqlRepo.WithByMysqlName(search.Database), - commonRepo.WithLikeName(search.Info), - commonRepo.WithOrderRuleBy(search.OrderBy, search.Order), - ) - var dtoMysqls []dto.MysqlDBInfo - for _, mysql := range mysqls { - var item dto.MysqlDBInfo - if err := copier.Copy(&item, &mysql); err != nil { - return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - dtoMysqls = append(dtoMysqls, item) - } - return total, dtoMysqls, err -} - -func (u *MysqlService) ListDBOption() ([]dto.MysqlOption, error) { - mysqls, err := mysqlRepo.List() - if err != nil { - return nil, err - } - - databases, err := databaseRepo.GetList(databaseRepo.WithTypeList("mysql,mariadb")) - if err != nil { - return nil, err - } - var dbs []dto.MysqlOption - for _, mysql := range mysqls { - var item dto.MysqlOption - if err := copier.Copy(&item, &mysql); err != nil { - return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - item.Database = mysql.MysqlName - for _, database := range databases { - if database.Name == item.Database { - item.Type = database.Type - } - } - dbs = append(dbs, item) - } - return dbs, err -} - -func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error) { - if cmd.CheckIllegal(req.Name, req.Username, req.Password, req.Format, req.Permission) { - return nil, buserr.New(constant.ErrCmdIllegal) - } - - mysql, _ := mysqlRepo.Get(commonRepo.WithByName(req.Name), mysqlRepo.WithByMysqlName(req.Database), databaseRepo.WithByFrom(req.From)) - if mysql.ID != 0 { - return nil, constant.ErrRecordExist - } - - var createItem model.DatabaseMysql - if err := copier.Copy(&createItem, &req); err != nil { - return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - - if req.From == "local" && req.Username == "root" { - return nil, errors.New("Cannot set root as user name") - } - - cli, version, err := LoadMysqlClientByFrom(req.Database) - if err != nil { - return nil, err - } - createItem.MysqlName = req.Database - defer cli.Close() - if err := cli.Create(client.CreateInfo{ - Name: req.Name, - Format: req.Format, - Username: req.Username, - Password: req.Password, - Permission: req.Permission, - Version: version, - Timeout: 300, - }); err != nil { - return nil, err - } - - global.LOG.Infof("create database %s successful!", req.Name) - if err := mysqlRepo.Create(ctx, &createItem); err != nil { - return nil, err - } - return &createItem, nil -} - -func (u *MysqlService) BindUser(req dto.BindUser) error { - dbItem, err := mysqlRepo.Get(mysqlRepo.WithByMysqlName(req.Database), commonRepo.WithByName(req.DB)) - if err != nil { - return err - } - cli, version, err := LoadMysqlClientByFrom(req.Database) - if err != nil { - return err - } - defer cli.Close() - - if err := cli.CreateUser(client.CreateInfo{ - Name: dbItem.Name, - Format: dbItem.Format, - Username: req.Username, - Password: req.Password, - Permission: req.Permission, - Version: version, - Timeout: 300, - }, false); err != nil { - return err - } - pass, err := encrypt.StringEncrypt(req.Password) - if err != nil { - return fmt.Errorf("decrypt database db password failed, err: %v", err) - } - if err := mysqlRepo.Update(dbItem.ID, map[string]interface{}{ - "username": req.Username, - "password": pass, - "permission": req.Permission, - }); err != nil { - return err - } - return nil -} - -func (u *MysqlService) LoadFromRemote(req dto.MysqlLoadDB) error { - client, version, err := LoadMysqlClientByFrom(req.Database) - if err != nil { - return err - } - - databases, err := mysqlRepo.List(mysqlRepo.WithByMysqlName(req.Database)) - if err != nil { - return err - } - datas, err := client.SyncDB(version) - if err != nil { - return err - } - deleteList := databases - for _, data := range datas { - hasOld := false - for i := 0; i < len(databases); i++ { - if strings.EqualFold(databases[i].Name, data.Name) && strings.EqualFold(databases[i].MysqlName, data.MysqlName) { - hasOld = true - if databases[i].IsDelete { - _ = mysqlRepo.Update(databases[i].ID, map[string]interface{}{"is_delete": false}) - } - deleteList = append(deleteList[:i], deleteList[i+1:]...) - break - } - } - if !hasOld { - var createItem model.DatabaseMysql - if err := copier.Copy(&createItem, &data); err != nil { - return errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - if err := mysqlRepo.Create(context.Background(), &createItem); err != nil { - return err - } - } - } - for _, delItem := range deleteList { - _ = mysqlRepo.Update(delItem.ID, map[string]interface{}{"is_delete": true}) - } - return nil -} - -func (u *MysqlService) UpdateDescription(req dto.UpdateDescription) error { - return mysqlRepo.Update(req.ID, map[string]interface{}{"description": req.Description}) -} - -func (u *MysqlService) DeleteCheck(req dto.MysqlDBDeleteCheck) ([]string, error) { - var appInUsed []string - db, err := mysqlRepo.Get(commonRepo.WithByID(req.ID)) - if err != nil { - return appInUsed, err - } - - if db.From == "local" { - app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Database) - if err != nil { - return appInUsed, err - } - apps, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(app.ID), appInstallResourceRepo.WithResourceId(db.ID)) - for _, app := range apps { - appInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(app.AppInstallId)) - if appInstall.ID != 0 { - appInUsed = append(appInUsed, appInstall.Name) - } - } - } else { - apps, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithResourceId(db.ID), appRepo.WithKey(req.Type)) - for _, app := range apps { - appInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(app.AppInstallId)) - if appInstall.ID != 0 { - appInUsed = append(appInUsed, appInstall.Name) - } - } - } - - return appInUsed, nil -} - -func (u *MysqlService) Delete(ctx context.Context, req dto.MysqlDBDelete) error { - db, err := mysqlRepo.Get(commonRepo.WithByID(req.ID)) - if err != nil && !req.ForceDelete { - return err - } - cli, version, err := LoadMysqlClientByFrom(req.Database) - if err != nil { - return err - } - defer cli.Close() - if err := cli.Delete(client.DeleteInfo{ - Name: db.Name, - Version: version, - Username: db.Username, - Permission: db.Permission, - Timeout: 300, - }); err != nil && !req.ForceDelete { - return err - } - - if req.DeleteBackup { - uploadDir := path.Join(global.CONF.System.BaseDir, fmt.Sprintf("1panel/uploads/database/%s/%s/%s", req.Type, req.Database, db.Name)) - if _, err := os.Stat(uploadDir); err == nil { - _ = os.RemoveAll(uploadDir) - } - localDir, err := loadLocalDir() - if err != nil && !req.ForceDelete { - return err - } - backupDir := path.Join(localDir, fmt.Sprintf("database/%s/%s/%s", req.Type, db.MysqlName, db.Name)) - if _, err := os.Stat(backupDir); err == nil { - _ = os.RemoveAll(backupDir) - } - _ = backupRepo.DeleteRecord(ctx, commonRepo.WithByType(req.Type), commonRepo.WithByName(req.Database), backupRepo.WithByDetailName(db.Name)) - global.LOG.Infof("delete database %s-%s backups successful", req.Database, db.Name) - } - - _ = mysqlRepo.Delete(ctx, commonRepo.WithByID(db.ID)) - return nil -} - -func (u *MysqlService) ChangePassword(req dto.ChangeDBInfo) error { - if cmd.CheckIllegal(req.Value) { - return buserr.New(constant.ErrCmdIllegal) - } - cli, version, err := LoadMysqlClientByFrom(req.Database) - if err != nil { - return err - } - defer cli.Close() - var ( - mysqlData model.DatabaseMysql - passwordInfo client.PasswordChangeInfo - ) - passwordInfo.Password = req.Value - passwordInfo.Timeout = 300 - passwordInfo.Version = version - - if req.ID != 0 { - mysqlData, err = mysqlRepo.Get(commonRepo.WithByID(req.ID)) - if err != nil { - return err - } - passwordInfo.Name = mysqlData.Name - passwordInfo.Username = mysqlData.Username - passwordInfo.Permission = mysqlData.Permission - } else { - passwordInfo.Username = "root" - } - if err := cli.ChangePassword(passwordInfo); err != nil { - return err - } - - if req.ID != 0 { - var appRess []model.AppInstallResource - if req.From == "local" { - app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Database) - if err != nil { - return err - } - appRess, _ = appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(app.ID), appInstallResourceRepo.WithResourceId(mysqlData.ID)) - } else { - appRess, _ = appInstallResourceRepo.GetBy(appInstallResourceRepo.WithResourceId(mysqlData.ID)) - } - for _, appRes := range appRess { - appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(appRes.AppInstallId)) - if err != nil { - return err - } - appModel, err := appRepo.GetFirst(commonRepo.WithByID(appInstall.AppId)) - if err != nil { - return err - } - - global.LOG.Infof("start to update mysql password used by app %s-%s", appModel.Key, appInstall.Name) - if err := updateInstallInfoInDB(appModel.Key, appInstall.Name, "user-password", req.Value); err != nil { - return err - } - } - global.LOG.Info("execute password change sql successful") - pass, err := encrypt.StringEncrypt(req.Value) - if err != nil { - return fmt.Errorf("decrypt database db password failed, err: %v", err) - } - _ = mysqlRepo.Update(mysqlData.ID, map[string]interface{}{"password": pass}) - return nil - } - - if err := updateInstallInfoInDB(req.Type, req.Database, "password", req.Value); err != nil { - return err - } - if req.From == "local" { - remote, err := databaseRepo.Get(commonRepo.WithByName(req.Database)) - if err != nil { - return err - } - pass, err := encrypt.StringEncrypt(req.Value) - if err != nil { - return fmt.Errorf("decrypt database password failed, err: %v", err) - } - _ = databaseRepo.Update(remote.ID, map[string]interface{}{"password": pass}) - } - return nil -} - -func (u *MysqlService) ChangeAccess(req dto.ChangeDBInfo) error { - if cmd.CheckIllegal(req.Value) { - return buserr.New(constant.ErrCmdIllegal) - } - cli, version, err := LoadMysqlClientByFrom(req.Database) - if err != nil { - return err - } - defer cli.Close() - var ( - mysqlData model.DatabaseMysql - accessInfo client.AccessChangeInfo - ) - accessInfo.Permission = req.Value - accessInfo.Timeout = 300 - accessInfo.Version = version - - if req.ID != 0 { - mysqlData, err = mysqlRepo.Get(commonRepo.WithByID(req.ID)) - if err != nil { - return err - } - accessInfo.Name = mysqlData.Name - accessInfo.Username = mysqlData.Username - accessInfo.Password = mysqlData.Password - accessInfo.OldPermission = mysqlData.Permission - } else { - accessInfo.Username = "root" - } - if err := cli.ChangeAccess(accessInfo); err != nil { - return err - } - - if mysqlData.ID != 0 { - _ = mysqlRepo.Update(mysqlData.ID, map[string]interface{}{"permission": req.Value}) - } - - return nil -} - -func (u *MysqlService) UpdateVariables(req dto.MysqlVariablesUpdate) error { - app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Database) - if err != nil { - return err - } - var files []string - - path := fmt.Sprintf("%s/%s/%s/conf/my.cnf", constant.AppInstallDir, req.Type, app.Name) - lineBytes, err := os.ReadFile(path) - if err != nil { - return err - } - files = strings.Split(string(lineBytes), "\n") - - group := "[mysqld]" - for _, info := range req.Variables { - if !strings.HasPrefix(app.Version, "5.7") && !strings.HasPrefix(app.Version, "5.6") { - if info.Param == "query_cache_size" { - continue - } - } - - if _, ok := info.Value.(float64); ok { - files = updateMyCnf(files, group, info.Param, common.LoadSizeUnit(info.Value.(float64))) - } else { - files = updateMyCnf(files, group, info.Param, info.Value) - } - } - file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0666) - if err != nil { - return err - } - defer file.Close() - _, err = file.WriteString(strings.Join(files, "\n")) - if err != nil { - return err - } - - if _, err := compose.Restart(fmt.Sprintf("%s/%s/%s/docker-compose.yml", constant.AppInstallDir, req.Type, app.Name)); err != nil { - return err - } - - return nil -} - -func (u *MysqlService) LoadRemoteAccess(req dto.OperationWithNameAndType) (bool, error) { - app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name) - if err != nil { - return false, err - } - hosts, err := executeSqlForRows(app.ContainerName, app.Key, app.Password, "select host from mysql.user where user='root';") - if err != nil { - return false, err - } - for _, host := range hosts { - if host == "%" { - return true, nil - } - } - - return false, nil -} - -func (u *MysqlService) LoadVariables(req dto.OperationWithNameAndType) (*dto.MysqlVariables, error) { - app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name) - if err != nil { - return nil, err - } - variableMap, err := executeSqlForMaps(app.ContainerName, app.Key, app.Password, "show global variables;") - if err != nil { - return nil, err - } - var info dto.MysqlVariables - arr, err := json.Marshal(variableMap) - if err != nil { - return nil, err - } - _ = json.Unmarshal(arr, &info) - return &info, nil -} - -func (u *MysqlService) LoadStatus(req dto.OperationWithNameAndType) (*dto.MysqlStatus, error) { - app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name) - if err != nil { - return nil, err - } - - statusMap, err := executeSqlForMaps(app.ContainerName, app.Key, app.Password, "show global status;") - if err != nil { - return nil, err - } - - var info dto.MysqlStatus - arr, err := json.Marshal(statusMap) - if err != nil { - return nil, err - } - _ = json.Unmarshal(arr, &info) - - if value, ok := statusMap["Run"]; ok { - uptime, _ := strconv.Atoi(value) - info.Run = time.Unix(time.Now().Unix()-int64(uptime), 0).Format(constant.DateTimeLayout) - } else { - if value, ok := statusMap["Uptime"]; ok { - uptime, _ := strconv.Atoi(value) - info.Run = time.Unix(time.Now().Unix()-int64(uptime), 0).Format(constant.DateTimeLayout) - } - } - - info.File = "OFF" - info.Position = "OFF" - rows, err := executeSqlForRows(app.ContainerName, app.Key, app.Password, "show master status;") - if err != nil { - return nil, err - } - if len(rows) > 2 { - itemValue := strings.Split(rows[1], "\t") - if len(itemValue) > 2 { - info.File = itemValue[0] - info.Position = itemValue[1] - } - } - - return &info, nil -} - -func executeSqlForMaps(containerName, dbType, password, command string) (map[string]string, error) { - cmd := exec.Command("docker", "exec", containerName, dbType, "-uroot", "-p"+password, "-e", command) - stdout, err := cmd.CombinedOutput() - stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "") - if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") { - return nil, errors.New(stdStr) - } - - rows := strings.Split(stdStr, "\n") - rowMap := make(map[string]string) - for _, v := range rows { - itemRow := strings.Split(v, "\t") - if len(itemRow) == 2 { - rowMap[itemRow[0]] = itemRow[1] - } - } - return rowMap, nil -} - -func executeSqlForRows(containerName, dbType, password, command string) ([]string, error) { - cmd := exec.Command("docker", "exec", containerName, dbType, "-uroot", "-p"+password, "-e", command) - stdout, err := cmd.CombinedOutput() - stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "") - if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") { - return nil, errors.New(stdStr) - } - return strings.Split(stdStr, "\n"), nil -} - -func updateMyCnf(oldFiles []string, group string, param string, value interface{}) []string { - isOn := false - hasGroup := false - hasKey := false - regItem, _ := regexp.Compile(`\[*\]`) - var newFiles []string - i := 0 - for _, line := range oldFiles { - i++ - if strings.HasPrefix(line, group) { - isOn = true - hasGroup = true - newFiles = append(newFiles, line) - continue - } - if !isOn { - newFiles = append(newFiles, line) - continue - } - if strings.HasPrefix(line, param+"=") || strings.HasPrefix(line, "# "+param+"=") { - newFiles = append(newFiles, fmt.Sprintf("%s=%v", param, value)) - hasKey = true - continue - } - if regItem.Match([]byte(line)) || i == len(oldFiles) { - isOn = false - if !hasKey { - newFiles = append(newFiles, fmt.Sprintf("%s=%v", param, value)) - } - newFiles = append(newFiles, line) - continue - } - newFiles = append(newFiles, line) - } - if !hasGroup { - newFiles = append(newFiles, group+"\n") - newFiles = append(newFiles, fmt.Sprintf("%s=%v\n", param, value)) - } - return newFiles -} - -func LoadMysqlClientByFrom(database string) (mysql.MysqlClient, string, error) { - var ( - dbInfo client.DBInfo - version string - err error - ) - - dbInfo.Timeout = 300 - databaseItem, err := databaseRepo.Get(commonRepo.WithByName(database)) - if err != nil { - return nil, "", err - } - dbInfo.Type = databaseItem.Type - dbInfo.From = databaseItem.From - dbInfo.Database = database - if dbInfo.From != "local" { - dbInfo.Address = databaseItem.Address - dbInfo.Port = databaseItem.Port - dbInfo.Username = databaseItem.Username - dbInfo.Password = databaseItem.Password - dbInfo.SSL = databaseItem.SSL - dbInfo.ClientKey = databaseItem.ClientKey - dbInfo.ClientCert = databaseItem.ClientCert - dbInfo.RootCert = databaseItem.RootCert - dbInfo.SkipVerify = databaseItem.SkipVerify - version = databaseItem.Version - - } else { - app, err := appInstallRepo.LoadBaseInfo(databaseItem.Type, database) - if err != nil { - return nil, "", err - } - dbInfo.Address = app.ContainerName - dbInfo.Username = "root" - dbInfo.Password = app.Password - version = app.Version - } - - cli, err := mysql.NewMysqlClient(dbInfo) - if err != nil { - return nil, "", err - } - return cli, version, nil -} diff --git a/backend/app/service/database_postgresql.go b/backend/app/service/database_postgresql.go deleted file mode 100644 index 07843447c..000000000 --- a/backend/app/service/database_postgresql.go +++ /dev/null @@ -1,432 +0,0 @@ -package service - -import ( - "context" - "fmt" - "os" - "path" - "strings" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/encrypt" - "github.com/1Panel-dev/1Panel/backend/utils/postgresql" - "github.com/1Panel-dev/1Panel/backend/utils/postgresql/client" - _ "github.com/jackc/pgx/v5/stdlib" - "github.com/jinzhu/copier" - "github.com/pkg/errors" -) - -type PostgresqlService struct{} - -type IPostgresqlService interface { - SearchWithPage(search dto.PostgresqlDBSearch) (int64, interface{}, error) - ListDBOption() ([]dto.PostgresqlOption, error) - BindUser(req dto.PostgresqlBindUser) error - Create(ctx context.Context, req dto.PostgresqlDBCreate) (*model.DatabasePostgresql, error) - LoadFromRemote(database string) error - ChangePrivileges(req dto.PostgresqlPrivileges) error - ChangePassword(info dto.ChangeDBInfo) error - UpdateDescription(req dto.UpdateDescription) error - DeleteCheck(req dto.PostgresqlDBDeleteCheck) ([]string, error) - Delete(ctx context.Context, req dto.PostgresqlDBDelete) error -} - -func NewIPostgresqlService() IPostgresqlService { - return &PostgresqlService{} -} - -func (u *PostgresqlService) SearchWithPage(search dto.PostgresqlDBSearch) (int64, interface{}, error) { - total, postgresqls, err := postgresqlRepo.Page(search.Page, search.PageSize, - postgresqlRepo.WithByPostgresqlName(search.Database), - commonRepo.WithLikeName(search.Info), - commonRepo.WithOrderRuleBy(search.OrderBy, search.Order), - ) - var dtoPostgresqls []dto.PostgresqlDBInfo - for _, pg := range postgresqls { - var item dto.PostgresqlDBInfo - if err := copier.Copy(&item, &pg); err != nil { - return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - dtoPostgresqls = append(dtoPostgresqls, item) - } - return total, dtoPostgresqls, err -} - -func (u *PostgresqlService) ListDBOption() ([]dto.PostgresqlOption, error) { - postgresqls, err := postgresqlRepo.List() - if err != nil { - return nil, err - } - - databases, err := databaseRepo.GetList(databaseRepo.WithTypeList("postgresql,mariadb")) - if err != nil { - return nil, err - } - var dbs []dto.PostgresqlOption - for _, pg := range postgresqls { - var item dto.PostgresqlOption - if err := copier.Copy(&item, &pg); err != nil { - return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - item.Database = pg.PostgresqlName - for _, database := range databases { - if database.Name == item.Database { - item.Type = database.Type - } - } - dbs = append(dbs, item) - } - return dbs, err -} - -func (u *PostgresqlService) BindUser(req dto.PostgresqlBindUser) error { - if cmd.CheckIllegal(req.Name, req.Username, req.Password) { - return buserr.New(constant.ErrCmdIllegal) - } - dbItem, err := postgresqlRepo.Get(postgresqlRepo.WithByPostgresqlName(req.Database), commonRepo.WithByName(req.Name)) - if err != nil { - return err - } - - pgClient, err := LoadPostgresqlClientByFrom(req.Database) - if err != nil { - return err - } - if err := pgClient.CreateUser(client.CreateInfo{ - Name: req.Name, - Username: req.Username, - Password: req.Password, - SuperUser: req.SuperUser, - Timeout: 300, - }, false); err != nil { - return err - } - pass, err := encrypt.StringEncrypt(req.Password) - if err != nil { - return fmt.Errorf("decrypt database db password failed, err: %v", err) - } - if err := postgresqlRepo.Update(dbItem.ID, map[string]interface{}{ - "username": req.Username, - "password": pass, - "super_user": req.SuperUser, - }); err != nil { - return err - } - return nil -} - -func (u *PostgresqlService) Create(ctx context.Context, req dto.PostgresqlDBCreate) (*model.DatabasePostgresql, error) { - if cmd.CheckIllegal(req.Name, req.Username, req.Password, req.Format) { - return nil, buserr.New(constant.ErrCmdIllegal) - } - - pgsql, _ := postgresqlRepo.Get(commonRepo.WithByName(req.Name), postgresqlRepo.WithByPostgresqlName(req.Database), databaseRepo.WithByFrom(req.From)) - if pgsql.ID != 0 { - return nil, constant.ErrRecordExist - } - - var createItem model.DatabasePostgresql - if err := copier.Copy(&createItem, &req); err != nil { - return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - - if req.From == "local" && req.Username == "root" { - return nil, errors.New("Cannot set root as user name") - } - - cli, err := LoadPostgresqlClientByFrom(req.Database) - if err != nil { - return nil, err - } - defer cli.Close() - - createItem.PostgresqlName = req.Database - defer cli.Close() - if err := cli.Create(client.CreateInfo{ - Name: req.Name, - Username: req.Username, - Password: req.Password, - SuperUser: req.SuperUser, - Timeout: 300, - }); err != nil { - return nil, err - } - - global.LOG.Infof("create database %s successful!", req.Name) - if err := postgresqlRepo.Create(ctx, &createItem); err != nil { - return nil, err - } - return &createItem, nil -} - -func LoadPostgresqlClientByFrom(database string) (postgresql.PostgresqlClient, error) { - var ( - dbInfo client.DBInfo - err error - ) - - dbInfo.Timeout = 300 - databaseItem, err := databaseRepo.Get(commonRepo.WithByName(database)) - if err != nil { - return nil, err - } - dbInfo.From = databaseItem.From - dbInfo.Database = database - if dbInfo.From != "local" { - dbInfo.Address = databaseItem.Address - dbInfo.Port = databaseItem.Port - dbInfo.Username = databaseItem.Username - dbInfo.Password = databaseItem.Password - } else { - app, err := appInstallRepo.LoadBaseInfo(databaseItem.Type, database) - if err != nil { - return nil, err - } - dbInfo.From = "local" - dbInfo.Address = app.ContainerName - dbInfo.Username = app.UserName - dbInfo.Password = app.Password - dbInfo.Port = uint(app.Port) - } - - cli, err := postgresql.NewPostgresqlClient(dbInfo) - if err != nil { - return nil, err - } - return cli, nil -} - -func (u *PostgresqlService) LoadFromRemote(database string) error { - client, err := LoadPostgresqlClientByFrom(database) - if err != nil { - return err - } - defer client.Close() - - databases, err := postgresqlRepo.List(postgresqlRepo.WithByPostgresqlName(database)) - if err != nil { - return err - } - datas, err := client.SyncDB() - if err != nil { - return err - } - deleteList := databases - for _, data := range datas { - hasOld := false - for i := 0; i < len(databases); i++ { - if strings.EqualFold(databases[i].Name, data.Name) && strings.EqualFold(databases[i].PostgresqlName, data.PostgresqlName) { - hasOld = true - if databases[i].IsDelete { - _ = postgresqlRepo.Update(databases[i].ID, map[string]interface{}{"is_delete": false}) - } - deleteList = append(deleteList[:i], deleteList[i+1:]...) - break - } - } - if !hasOld { - var createItem model.DatabasePostgresql - if err := copier.Copy(&createItem, &data); err != nil { - return errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - if err := postgresqlRepo.Create(context.Background(), &createItem); err != nil { - return err - } - } - } - for _, delItem := range deleteList { - _ = postgresqlRepo.Update(delItem.ID, map[string]interface{}{"is_delete": true}) - } - return nil -} - -func (u *PostgresqlService) UpdateDescription(req dto.UpdateDescription) error { - return postgresqlRepo.Update(req.ID, map[string]interface{}{"description": req.Description}) -} - -func (u *PostgresqlService) DeleteCheck(req dto.PostgresqlDBDeleteCheck) ([]string, error) { - var appInUsed []string - db, err := postgresqlRepo.Get(commonRepo.WithByID(req.ID)) - if err != nil { - return appInUsed, err - } - - if db.From == "local" { - app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Database) - if err != nil { - return appInUsed, err - } - apps, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(app.ID), appInstallResourceRepo.WithResourceId(db.ID)) - for _, app := range apps { - appInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(app.AppInstallId)) - if appInstall.ID != 0 { - appInUsed = append(appInUsed, appInstall.Name) - } - } - } else { - apps, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithResourceId(db.ID), appRepo.WithKey(req.Type)) - for _, app := range apps { - appInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(app.AppInstallId)) - if appInstall.ID != 0 { - appInUsed = append(appInUsed, appInstall.Name) - } - } - } - - return appInUsed, nil -} - -func (u *PostgresqlService) Delete(ctx context.Context, req dto.PostgresqlDBDelete) error { - db, err := postgresqlRepo.Get(commonRepo.WithByID(req.ID)) - if err != nil && !req.ForceDelete { - return err - } - cli, err := LoadPostgresqlClientByFrom(req.Database) - if err != nil { - return err - } - defer cli.Close() - if err := cli.Delete(client.DeleteInfo{ - Name: db.Name, - Username: db.Username, - ForceDelete: req.ForceDelete, - Timeout: 300, - }); err != nil && !req.ForceDelete { - return err - } - - if req.DeleteBackup { - uploadDir := path.Join(global.CONF.System.BaseDir, fmt.Sprintf("1panel/uploads/database/%s/%s/%s", req.Type, req.Database, db.Name)) - if _, err := os.Stat(uploadDir); err == nil { - _ = os.RemoveAll(uploadDir) - } - localDir, err := loadLocalDir() - if err != nil && !req.ForceDelete { - return err - } - backupDir := path.Join(localDir, fmt.Sprintf("database/%s/%s/%s", req.Type, db.PostgresqlName, db.Name)) - if _, err := os.Stat(backupDir); err == nil { - _ = os.RemoveAll(backupDir) - } - _ = backupRepo.DeleteRecord(ctx, commonRepo.WithByType(req.Type), commonRepo.WithByName(req.Database), backupRepo.WithByDetailName(db.Name)) - global.LOG.Infof("delete database %s-%s backups successful", req.Database, db.Name) - } - - _ = postgresqlRepo.Delete(ctx, commonRepo.WithByID(db.ID)) - return nil -} - -func (u *PostgresqlService) ChangePrivileges(req dto.PostgresqlPrivileges) error { - if cmd.CheckIllegal(req.Database, req.Username) { - return buserr.New(constant.ErrCmdIllegal) - } - dbItem, err := postgresqlRepo.Get(postgresqlRepo.WithByPostgresqlName(req.Database), commonRepo.WithByName(req.Name)) - if err != nil { - return err - } - cli, err := LoadPostgresqlClientByFrom(req.Database) - if err != nil { - return err - } - defer cli.Close() - - if err := cli.ChangePrivileges(client.Privileges{Username: req.Username, SuperUser: req.SuperUser, Timeout: 300}); err != nil { - return err - } - if err := postgresqlRepo.Update(dbItem.ID, map[string]interface{}{ - "super_user": req.SuperUser, - }); err != nil { - return err - } - return nil -} - -func (u *PostgresqlService) ChangePassword(req dto.ChangeDBInfo) error { - if cmd.CheckIllegal(req.Value) { - return buserr.New(constant.ErrCmdIllegal) - } - cli, err := LoadPostgresqlClientByFrom(req.Database) - if err != nil { - return err - } - defer cli.Close() - var ( - postgresqlData model.DatabasePostgresql - passwordInfo client.PasswordChangeInfo - ) - passwordInfo.Password = req.Value - passwordInfo.Timeout = 300 - - if req.ID != 0 { - postgresqlData, err = postgresqlRepo.Get(commonRepo.WithByID(req.ID)) - if err != nil { - return err - } - passwordInfo.Username = postgresqlData.Username - } else { - dbItem, err := databaseRepo.Get(commonRepo.WithByType(req.Type), commonRepo.WithByFrom(req.From)) - if err != nil { - return err - } - passwordInfo.Username = dbItem.Username - } - if err := cli.ChangePassword(passwordInfo); err != nil { - return err - } - - if req.ID != 0 { - var appRess []model.AppInstallResource - if req.From == "local" { - app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Database) - if err != nil { - return err - } - appRess, _ = appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(app.ID), appInstallResourceRepo.WithResourceId(postgresqlData.ID)) - } else { - appRess, _ = appInstallResourceRepo.GetBy(appInstallResourceRepo.WithResourceId(postgresqlData.ID)) - } - for _, appRes := range appRess { - appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(appRes.AppInstallId)) - if err != nil { - return err - } - appModel, err := appRepo.GetFirst(commonRepo.WithByID(appInstall.AppId)) - if err != nil { - return err - } - - global.LOG.Infof("start to update postgresql password used by app %s-%s", appModel.Key, appInstall.Name) - if err := updateInstallInfoInDB(appModel.Key, appInstall.Name, "user-password", req.Value); err != nil { - return err - } - } - global.LOG.Info("execute password change sql successful") - pass, err := encrypt.StringEncrypt(req.Value) - if err != nil { - return fmt.Errorf("decrypt database db password failed, err: %v", err) - } - _ = postgresqlRepo.Update(postgresqlData.ID, map[string]interface{}{"password": pass}) - return nil - } - - if err := updateInstallInfoInDB(req.Type, req.Database, "password", req.Value); err != nil { - return err - } - if req.From == "local" { - remote, err := databaseRepo.Get(commonRepo.WithByName(req.Database)) - if err != nil { - return err - } - pass, err := encrypt.StringEncrypt(req.Value) - if err != nil { - return fmt.Errorf("decrypt database password failed, err: %v", err) - } - _ = databaseRepo.Update(remote.ID, map[string]interface{}{"password": pass}) - } - return nil -} diff --git a/backend/app/service/database_redis.go b/backend/app/service/database_redis.go deleted file mode 100644 index 15149f447..000000000 --- a/backend/app/service/database_redis.go +++ /dev/null @@ -1,294 +0,0 @@ -package service - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "os" - "os/exec" - "strings" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/compose" - "github.com/1Panel-dev/1Panel/backend/utils/docker" - "github.com/1Panel-dev/1Panel/backend/utils/encrypt" - "github.com/docker/docker/api/types/container" - _ "github.com/go-sql-driver/mysql" -) - -type RedisService struct{} - -type IRedisService interface { - UpdateConf(req dto.RedisConfUpdate) error - UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error - ChangePassword(info dto.ChangeRedisPass) error - - LoadStatus(req dto.OperationWithName) (*dto.RedisStatus, error) - LoadConf(req dto.OperationWithName) (*dto.RedisConf, error) - LoadPersistenceConf(req dto.OperationWithName) (*dto.RedisPersistence, error) - - CheckHasCli() bool - InstallCli() error -} - -func NewIRedisService() IRedisService { - return &RedisService{} -} - -func (u *RedisService) UpdateConf(req dto.RedisConfUpdate) error { - redisInfo, err := appInstallRepo.LoadBaseInfo("redis", req.Database) - if err != nil { - return err - } - - var confs []redisConfig - confs = append(confs, redisConfig{key: "timeout", value: req.Timeout}) - confs = append(confs, redisConfig{key: "maxclients", value: req.Maxclients}) - confs = append(confs, redisConfig{key: "maxmemory", value: req.Maxmemory}) - if err := confSet(redisInfo.Name, "", confs); err != nil { - return err - } - if _, err := compose.Restart(fmt.Sprintf("%s/redis/%s/docker-compose.yml", constant.AppInstallDir, redisInfo.Name)); err != nil { - return err - } - - return nil -} - -func (u *RedisService) CheckHasCli() bool { - client, err := docker.NewDockerClient() - if err != nil { - return false - } - defer client.Close() - containerLists, err := client.ContainerList(context.Background(), container.ListOptions{}) - if err != nil { - return false - } - for _, item := range containerLists { - if strings.ReplaceAll(item.Names[0], "/", "") == "1Panel-redis-cli-tools" { - return true - } - } - return false -} - -func (u *RedisService) InstallCli() error { - item := dto.ContainerOperate{ - Name: "1Panel-redis-cli-tools", - Image: "redis:7.2.4", - Network: "1panel-network", - } - return NewIContainerService().ContainerCreate(item) -} - -func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error { - if err := updateInstallInfoInDB("redis", req.Database, "password", req.Value); err != nil { - return err - } - remote, err := databaseRepo.Get(commonRepo.WithByName(req.Database)) - if err != nil { - return err - } - if remote.From == "local" { - pass, err := encrypt.StringEncrypt(req.Value) - if err != nil { - return fmt.Errorf("decrypt database password failed, err: %v", err) - } - _ = databaseRepo.Update(remote.ID, map[string]interface{}{"password": pass}) - } - - return nil -} - -func (u *RedisService) UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error { - redisInfo, err := appInstallRepo.LoadBaseInfo("redis", req.Database) - if err != nil { - return err - } - - var confs []redisConfig - if req.Type == "rbd" { - confs = append(confs, redisConfig{key: "save", value: req.Save}) - } else { - confs = append(confs, redisConfig{key: "appendonly", value: req.Appendonly}) - confs = append(confs, redisConfig{key: "appendfsync", value: req.Appendfsync}) - } - if err := confSet(redisInfo.Name, req.Type, confs); err != nil { - return err - } - if _, err := compose.Restart(fmt.Sprintf("%s/redis/%s/docker-compose.yml", constant.AppInstallDir, redisInfo.Name)); err != nil { - return err - } - - return nil -} - -func (u *RedisService) LoadStatus(req dto.OperationWithName) (*dto.RedisStatus, error) { - redisInfo, err := appInstallRepo.LoadBaseInfo("redis", req.Name) - if err != nil { - return nil, err - } - commands := append(redisExec(redisInfo.ContainerName, redisInfo.Password), "info") - cmd := exec.Command("docker", commands...) - stdout, err := cmd.CombinedOutput() - if err != nil { - return nil, errors.New(string(stdout)) - } - rows := strings.Split(string(stdout), "\r\n") - rowMap := make(map[string]string) - for _, v := range rows { - itemRow := strings.Split(v, ":") - if len(itemRow) == 2 { - rowMap[itemRow[0]] = itemRow[1] - } - } - var info dto.RedisStatus - arr, err := json.Marshal(rowMap) - if err != nil { - return nil, err - } - _ = json.Unmarshal(arr, &info) - return &info, nil -} - -func (u *RedisService) LoadConf(req dto.OperationWithName) (*dto.RedisConf, error) { - redisInfo, err := appInstallRepo.LoadBaseInfo("redis", req.Name) - if err != nil { - return nil, err - } - - var item dto.RedisConf - item.ContainerName = redisInfo.ContainerName - item.Name = redisInfo.Name - item.Port = redisInfo.Port - item.Requirepass = redisInfo.Password - item.Timeout, _ = configGetStr(redisInfo.ContainerName, redisInfo.Password, "timeout") - item.Maxclients, _ = configGetStr(redisInfo.ContainerName, redisInfo.Password, "maxclients") - item.Maxmemory, _ = configGetStr(redisInfo.ContainerName, redisInfo.Password, "maxmemory") - return &item, nil -} - -func (u *RedisService) LoadPersistenceConf(req dto.OperationWithName) (*dto.RedisPersistence, error) { - redisInfo, err := appInstallRepo.LoadBaseInfo("redis", req.Name) - if err != nil { - return nil, err - } - var item dto.RedisPersistence - if item.Appendonly, err = configGetStr(redisInfo.ContainerName, redisInfo.Password, "appendonly"); err != nil { - return nil, err - } - if item.Appendfsync, err = configGetStr(redisInfo.ContainerName, redisInfo.Password, "appendfsync"); err != nil { - return nil, err - } - if item.Save, err = configGetStr(redisInfo.ContainerName, redisInfo.Password, "save"); err != nil { - return nil, err - } - return &item, nil -} - -func configGetStr(containerName, password, param string) (string, error) { - commands := append(redisExec(containerName, password), []string{"config", "get", param}...) - cmd := exec.Command("docker", commands...) - stdout, err := cmd.CombinedOutput() - if err != nil { - return "", errors.New(string(stdout)) - } - rows := strings.Split(string(stdout), "\r\n") - for _, v := range rows { - itemRow := strings.Split(v, "\n") - if len(itemRow) == 3 { - return itemRow[1], nil - } - } - return "", nil -} - -type redisConfig struct { - key string - value string -} - -func confSet(redisName string, updateType string, changeConf []redisConfig) error { - path := fmt.Sprintf("%s/redis/%s/conf/redis.conf", constant.AppInstallDir, redisName) - lineBytes, err := os.ReadFile(path) - if err != nil { - return err - } - files := strings.Split(string(lineBytes), "\n") - - startIndex, endIndex, emptyLine := 0, 0, 0 - var newFiles []string - for i := 0; i < len(files); i++ { - if files[i] == "# Redis configuration rewrite by 1Panel" { - startIndex = i - newFiles = append(newFiles, files[i]) - continue - } - if files[i] == "# End Redis configuration rewrite by 1Panel" { - endIndex = i - break - } - if startIndex == 0 && strings.HasPrefix(files[i], "save ") { - newFiles = append(newFiles, "# "+files[i]) - continue - } - if startIndex != 0 && endIndex == 0 && (len(files[i]) == 0 || (updateType == "rbd" && strings.HasPrefix(files[i], "save "))) { - emptyLine++ - continue - } - newFiles = append(newFiles, files[i]) - } - endIndex = endIndex - emptyLine - for _, item := range changeConf { - if item.key == "save" { - saveVal := strings.Split(item.value, ",") - for i := 0; i < len(saveVal); i++ { - newFiles = append(newFiles, "save "+saveVal[i]) - } - continue - } - - isExist := false - for i := startIndex; i < endIndex; i++ { - if strings.HasPrefix(newFiles[i], item.key) || strings.HasPrefix(newFiles[i], "# "+item.key) { - if item.value == "0" || len(item.value) == 0 { - newFiles[i] = fmt.Sprintf("# %s %s", item.key, item.value) - } else { - newFiles[i] = fmt.Sprintf("%s %s", item.key, item.value) - } - isExist = true - break - } - } - if !isExist { - if item.value == "0" || len(item.value) == 0 { - newFiles = append(newFiles, fmt.Sprintf("# %s %s", item.key, item.value)) - } else { - newFiles = append(newFiles, fmt.Sprintf("%s %s", item.key, item.value)) - } - } - } - newFiles = append(newFiles, "# End Redis configuration rewrite by 1Panel") - - file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0666) - if err != nil { - return err - } - defer file.Close() - _, err = file.WriteString(strings.Join(newFiles, "\n")) - if err != nil { - return err - } - return nil -} - -func redisExec(containerName, password string) []string { - cmds := []string{"exec", containerName, "redis-cli", "-a", password, "--no-auth-warning"} - if len(password) == 0 { - cmds = []string{"exec", containerName, "redis-cli"} - } - return cmds -} diff --git a/backend/app/service/device.go b/backend/app/service/device.go deleted file mode 100644 index 8fcf8a8d7..000000000 --- a/backend/app/service/device.go +++ /dev/null @@ -1,431 +0,0 @@ -package service - -import ( - "bufio" - "errors" - "fmt" - "net" - "os" - "path" - "strconv" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/1Panel-dev/1Panel/backend/utils/ntp" - "github.com/shirou/gopsutil/v3/mem" -) - -const defaultDNSPath = "/etc/resolv.conf" -const defaultHostPath = "/etc/hosts" -const defaultFstab = "/etc/fstab" - -type DeviceService struct{} - -type IDeviceService interface { - LoadBaseInfo() (dto.DeviceBaseInfo, error) - Update(key, value string) error - UpdateHosts(req []dto.HostHelper) error - UpdatePasswd(req dto.ChangePasswd) error - UpdateSwap(req dto.SwapHelper) error - UpdateByConf(req dto.UpdateByNameAndFile) error - LoadTimeZone() ([]string, error) - CheckDNS(key, value string) (bool, error) - LoadConf(name string) (string, error) - - Scan() dto.CleanData - Clean(req []dto.Clean) - CleanForCronjob() (string, error) -} - -func NewIDeviceService() IDeviceService { - return &DeviceService{} -} - -func (u *DeviceService) LoadBaseInfo() (dto.DeviceBaseInfo, error) { - var baseInfo dto.DeviceBaseInfo - baseInfo.LocalTime = time.Now().Format("2006-01-02 15:04:05 MST -0700") - baseInfo.TimeZone = common.LoadTimeZoneByCmd() - baseInfo.DNS = loadDNS() - baseInfo.Hosts = loadHosts() - baseInfo.Hostname = loadHostname() - baseInfo.User = loadUser() - ntp, err := settingRepo.Get(settingRepo.WithByKey("NtpSite")) - if err == nil { - baseInfo.Ntp = ntp.Value - } - - swapInfo, err := mem.SwapMemory() - if err != nil { - return baseInfo, err - } - baseInfo.SwapMemoryTotal = swapInfo.Total - baseInfo.SwapMemoryAvailable = swapInfo.Free - baseInfo.SwapMemoryUsed = swapInfo.Used - if baseInfo.SwapMemoryTotal != 0 { - baseInfo.SwapDetails = loadSwap() - } - disks := loadDiskInfo() - for _, item := range disks { - baseInfo.MaxSize += item.Free - } - - return baseInfo, nil -} - -func (u *DeviceService) LoadTimeZone() ([]string, error) { - std, err := cmd.Exec("timedatectl list-timezones") - if err != nil { - return []string{}, err - } - return strings.Split(std, "\n"), nil -} - -func (u *DeviceService) CheckDNS(key, value string) (bool, error) { - content, err := os.ReadFile(defaultDNSPath) - if err != nil { - return false, err - } - defer func() { _ = u.UpdateByConf(dto.UpdateByNameAndFile{Name: "DNS", File: string(content)}) }() - if key == "form" { - if err := u.Update("DNS", value); err != nil { - return false, err - } - } else { - if err := u.UpdateByConf(dto.UpdateByNameAndFile{Name: "DNS", File: value}); err != nil { - return false, err - } - } - - conn, err := net.DialTimeout("ip4:icmp", "www.baidu.com", time.Second*2) - if err != nil { - return false, err - } - defer conn.Close() - - return true, nil -} - -func (u *DeviceService) Update(key, value string) error { - switch key { - case "TimeZone": - if cmd.CheckIllegal(value) { - return buserr.New(constant.ErrCmdIllegal) - } - if err := ntp.UpdateSystemTimeZone(value); err != nil { - return err - } - go func() { - _, err := cmd.Exec("systemctl restart 1panel.service") - if err != nil { - global.LOG.Errorf("restart system for new time zone failed, err: %v", err) - } - }() - case "DNS": - if err := updateDNS(strings.Split(value, ",")); err != nil { - return err - } - case "Hostname": - if cmd.CheckIllegal(value) { - return buserr.New(constant.ErrCmdIllegal) - } - std, err := cmd.Execf("%s hostnamectl set-hostname %s", cmd.SudoHandleCmd(), value) - if err != nil { - return errors.New(std) - } - case "Ntp", "LocalTime": - if cmd.CheckIllegal(value) { - return buserr.New(constant.ErrCmdIllegal) - } - ntpValue := value - if key == "LocalTime" { - ntpItem, err := settingRepo.Get(settingRepo.WithByKey("NtpSite")) - if err != nil { - return err - } - ntpValue = ntpItem.Value - } else { - if err := settingRepo.Update("NtpSite", ntpValue); err != nil { - return err - } - } - ntime, err := ntp.GetRemoteTime(ntpValue) - if err != nil { - return err - } - ts := ntime.Format(constant.DateTimeLayout) - if err := ntp.UpdateSystemTime(ts); err != nil { - return err - } - default: - return fmt.Errorf("not support such key %s", key) - } - return nil -} - -func (u *DeviceService) UpdateHosts(req []dto.HostHelper) error { - conf, err := os.ReadFile(defaultHostPath) - if err != nil { - return fmt.Errorf("read namesever conf of %s failed, err: %v", defaultHostPath, err) - } - lines := strings.Split(string(conf), "\n") - newFile := "" - for _, line := range lines { - if len(line) == 0 { - continue - } - parts := strings.Fields(line) - if len(parts) < 2 { - newFile += line + "\n" - continue - } - for index, item := range req { - if item.IP == parts[0] && item.Host == strings.Join(parts[1:], " ") { - newFile += line + "\n" - req = append(req[:index], req[index+1:]...) - break - } - } - } - for _, item := range req { - newFile += fmt.Sprintf("%s %s \n", item.IP, item.Host) - } - file, err := os.OpenFile(defaultHostPath, os.O_WRONLY|os.O_TRUNC, 0640) - if err != nil { - return err - } - defer file.Close() - write := bufio.NewWriter(file) - _, _ = write.WriteString(newFile) - write.Flush() - return nil -} - -func (u *DeviceService) UpdatePasswd(req dto.ChangePasswd) error { - if cmd.CheckIllegal(req.User, req.Passwd) { - return buserr.New(constant.ErrCmdIllegal) - } - std, err := cmd.Execf("%s echo '%s:%s' | %s chpasswd", cmd.SudoHandleCmd(), req.User, req.Passwd, cmd.SudoHandleCmd()) - if err != nil { - if strings.Contains(err.Error(), "does not exist") { - return buserr.New(constant.ErrNotExistUser) - } - return errors.New(std) - } - return nil -} - -func (u *DeviceService) UpdateSwap(req dto.SwapHelper) error { - if cmd.CheckIllegal(req.Path) { - return buserr.New(constant.ErrCmdIllegal) - } - if !req.IsNew { - std, err := cmd.Execf("%s swapoff %s", cmd.SudoHandleCmd(), req.Path) - if err != nil { - return fmt.Errorf("handle swapoff %s failed, err: %s", req.Path, std) - } - } - if req.Size == 0 { - if req.Path == path.Join(global.CONF.System.BaseDir, ".1panel_swap") { - _ = os.Remove(path.Join(global.CONF.System.BaseDir, ".1panel_swap")) - } - return operateSwapWithFile(true, req) - } - std1, err := cmd.Execf("%s dd if=/dev/zero of=%s bs=1024 count=%d", cmd.SudoHandleCmd(), req.Path, req.Size) - if err != nil { - return fmt.Errorf("handle dd path %s failed, err: %s", req.Path, std1) - } - std2, err := cmd.Execf("%s mkswap -f %s", cmd.SudoHandleCmd(), req.Path) - if err != nil { - return fmt.Errorf("handle dd path %s failed, err: %s", req.Path, std2) - } - std3, err := cmd.Execf("%s swapon %s", cmd.SudoHandleCmd(), req.Path) - if err != nil { - _, _ = cmd.Execf("%s swapoff %s", cmd.SudoHandleCmd(), req.Path) - return fmt.Errorf("handle dd path %s failed, err: %s", req.Path, std3) - } - return operateSwapWithFile(false, req) -} - -func (u *DeviceService) LoadConf(name string) (string, error) { - pathItem := "" - switch name { - case "DNS": - pathItem = defaultDNSPath - case "Hosts": - pathItem = defaultHostPath - default: - return "", fmt.Errorf("not support such name %s", name) - } - if _, err := os.Stat(pathItem); err != nil { - return "", err - } - content, err := os.ReadFile(pathItem) - if err != nil { - return "", err - } - return string(content), nil -} - -func (u *DeviceService) UpdateByConf(req dto.UpdateByNameAndFile) error { - if req.Name != "DNS" && req.Name != "Hosts" { - return fmt.Errorf("not support such name %s", req.Name) - } - path := defaultDNSPath - if req.Name == "Hosts" { - path = defaultHostPath - } - file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640) - if err != nil { - return err - } - defer file.Close() - write := bufio.NewWriter(file) - _, _ = write.WriteString(req.File) - write.Flush() - return nil -} - -func loadDNS() []string { - var list []string - dnsConf, err := os.ReadFile(defaultDNSPath) - if err == nil { - lines := strings.Split(string(dnsConf), "\n") - for _, line := range lines { - if strings.HasPrefix(line, "nameserver ") { - list = append(list, strings.TrimPrefix(line, "nameserver ")) - } - } - } - return list -} - -func updateDNS(list []string) error { - conf, err := os.ReadFile(defaultDNSPath) - if err != nil { - return fmt.Errorf("read nameserver conf of %s failed, err: %v", defaultDNSPath, err) - } - lines := strings.Split(string(conf), "\n") - newFile := "" - for _, line := range lines { - if len(line) == 0 { - continue - } - parts := strings.Fields(line) - if len(parts) < 2 || parts[0] != "nameserver" { - newFile += line + "\n" - continue - } - itemNs := strings.Join(parts[1:], " ") - for index, item := range list { - if item == itemNs { - newFile += line + "\n" - list = append(list[:index], list[index+1:]...) - break - } - } - } - for _, item := range list { - if len(item) != 0 { - newFile += fmt.Sprintf("nameserver %s \n", item) - } - } - file, err := os.OpenFile(defaultDNSPath, os.O_WRONLY|os.O_TRUNC, 0640) - if err != nil { - return err - } - defer file.Close() - write := bufio.NewWriter(file) - _, _ = write.WriteString(newFile) - write.Flush() - return nil -} - -func loadHosts() []dto.HostHelper { - var list []dto.HostHelper - hostConf, err := os.ReadFile(defaultHostPath) - if err == nil { - lines := strings.Split(string(hostConf), "\n") - for _, line := range lines { - parts := strings.Fields(line) - if len(parts) < 2 || strings.HasPrefix(strings.TrimPrefix(line, " "), "#") { - continue - } - list = append(list, dto.HostHelper{IP: parts[0], Host: strings.Join(parts[1:], " ")}) - } - } - return list -} - -func loadHostname() string { - std, err := cmd.Exec("hostname") - if err != nil { - return "" - } - return strings.ReplaceAll(std, "\n", "") -} - -func loadUser() string { - std, err := cmd.Exec("whoami") - if err != nil { - return "" - } - return strings.ReplaceAll(std, "\n", "") -} - -func loadSwap() []dto.SwapHelper { - var data []dto.SwapHelper - std, err := cmd.Execf("%s swapon --summary", cmd.SudoHandleCmd()) - if err != nil { - return data - } - lines := strings.Split(std, "\n") - for index, line := range lines { - if index == 0 { - continue - } - parts := strings.Fields(line) - if len(parts) < 5 { - continue - } - sizeItem, _ := strconv.Atoi(parts[2]) - data = append(data, dto.SwapHelper{Path: parts[0], Size: uint64(sizeItem), Used: parts[3]}) - } - return data -} - -func operateSwapWithFile(delete bool, req dto.SwapHelper) error { - conf, err := os.ReadFile(defaultFstab) - if err != nil { - return fmt.Errorf("read file %s failed, err: %v", defaultFstab, err) - } - lines := strings.Split(string(conf), "\n") - newFile := "" - for _, line := range lines { - if len(line) == 0 { - continue - } - parts := strings.Fields(line) - if len(parts) == 6 && parts[0] == req.Path { - continue - } - newFile += line + "\n" - } - if !delete { - newFile += fmt.Sprintf("%s swap swap defaults 0 0\n", req.Path) - } - file, err := os.OpenFile(defaultFstab, os.O_WRONLY|os.O_TRUNC, 0640) - if err != nil { - return err - } - defer file.Close() - write := bufio.NewWriter(file) - _, _ = write.WriteString(newFile) - write.Flush() - return nil -} diff --git a/backend/app/service/device_clean.go b/backend/app/service/device_clean.go deleted file mode 100644 index f6dfd53a4..000000000 --- a/backend/app/service/device_clean.go +++ /dev/null @@ -1,735 +0,0 @@ -package service - -import ( - "context" - "fmt" - "os" - "path" - "sort" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/docker" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/common" - fileUtils "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/google/uuid" -) - -const ( - upgradePath = "1panel/tmp/upgrade" - snapshotTmpPath = "1panel/tmp/system" - rollbackPath = "1panel/tmp" - cachePath = "1panel/cache" - oldOriginalPath = "original" - oldAppBackupPath = "1panel/resource/apps_bak" - oldDownloadPath = "1panel/tmp/download" - oldUpgradePath = "1panel/tmp" - tmpUploadPath = "1panel/tmp/upload" - uploadPath = "1panel/uploads" - downloadPath = "1panel/download" - logPath = "1panel/log" - dockerLogPath = "1panel/tmp/docker_logs" - taskPath = "1panel/task" -) - -func (u *DeviceService) Scan() dto.CleanData { - var ( - SystemClean dto.CleanData - treeData []dto.CleanTree - ) - fileOp := fileUtils.NewFileOp() - - originalPath := path.Join(global.CONF.System.BaseDir, "1panel_original") - originalSize, _ := fileOp.GetDirSize(originalPath) - treeData = append(treeData, dto.CleanTree{ - ID: uuid.NewString(), - Label: "1panel_original", - Size: uint64(originalSize), - IsCheck: true, - IsRecommend: true, - Type: "1panel_original", - Children: loadTreeWithDir(true, "1panel_original", originalPath, fileOp), - }) - - upgradePath := path.Join(global.CONF.System.BaseDir, upgradePath) - upgradeSize, _ := fileOp.GetDirSize(upgradePath) - treeData = append(treeData, dto.CleanTree{ - ID: uuid.NewString(), - Label: "upgrade", - Size: uint64(upgradeSize), - IsCheck: false, - IsRecommend: true, - Type: "upgrade", - Children: loadTreeWithDir(true, "upgrade", upgradePath, fileOp), - }) - - snapTree := loadSnapshotTree(fileOp) - snapSize := uint64(0) - for _, snap := range snapTree { - snapSize += snap.Size - } - treeData = append(treeData, dto.CleanTree{ - ID: uuid.NewString(), - Label: "snapshot", - Size: snapSize, - IsCheck: true, - IsRecommend: true, - Type: "snapshot", - Children: snapTree, - }) - - rollBackTree := loadRollBackTree(fileOp) - rollbackSize := uint64(0) - for _, rollback := range rollBackTree { - rollbackSize += rollback.Size - } - treeData = append(treeData, dto.CleanTree{ - ID: uuid.NewString(), - Label: "rollback", - Size: rollbackSize, - IsCheck: true, - IsRecommend: true, - Type: "rollback", - Children: rollBackTree, - }) - - cachePath := path.Join(global.CONF.System.BaseDir, cachePath) - cacheSize, _ := fileOp.GetDirSize(cachePath) - treeData = append(treeData, dto.CleanTree{ - ID: uuid.NewString(), - Label: "cache", - Size: uint64(cacheSize), - IsCheck: false, - IsRecommend: false, - Type: "cache", - }) - - unusedTree := loadUnusedFile(fileOp) - unusedSize := uint64(0) - for _, unused := range unusedTree { - unusedSize += unused.Size - } - treeData = append(treeData, dto.CleanTree{ - ID: uuid.NewString(), - Label: "unused", - Size: unusedSize, - IsCheck: true, - IsRecommend: true, - Type: "unused", - Children: unusedTree, - }) - SystemClean.SystemClean = treeData - - uploadTreeData := loadUploadTree(fileOp) - SystemClean.UploadClean = append(SystemClean.UploadClean, uploadTreeData...) - - downloadTreeData := loadDownloadTree(fileOp) - SystemClean.DownloadClean = append(SystemClean.DownloadClean, downloadTreeData...) - - logTree := loadLogTree(fileOp) - SystemClean.SystemLogClean = append(SystemClean.SystemLogClean, logTree...) - - containerTree := loadContainerTree() - SystemClean.ContainerClean = append(SystemClean.ContainerClean, containerTree...) - - return SystemClean -} - -func (u *DeviceService) Clean(req []dto.Clean) { - size := uint64(0) - restart := false - for _, item := range req { - size += item.Size - switch item.TreeType { - case "1panel_original": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, "1panel_original", item.Name)) - - case "upgrade": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, upgradePath, item.Name)) - - case "snapshot": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, snapshotTmpPath, item.Name)) - dropFileOrDir(path.Join(global.CONF.System.Backup, "system", item.Name)) - case "snapshot_tmp": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, snapshotTmpPath, item.Name)) - case "snapshot_local": - dropFileOrDir(path.Join(global.CONF.System.Backup, "system", item.Name)) - - case "rollback": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, rollbackPath, "app")) - dropFileOrDir(path.Join(global.CONF.System.BaseDir, rollbackPath, "database")) - dropFileOrDir(path.Join(global.CONF.System.BaseDir, rollbackPath, "website")) - case "rollback_app": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, rollbackPath, "app", item.Name)) - case "rollback_database": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, rollbackPath, "database", item.Name)) - case "rollback_website": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, rollbackPath, "website", item.Name)) - - case "cache": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, cachePath, item.Name)) - restart = true - - case "unused": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, oldOriginalPath)) - dropFileOrDir(path.Join(global.CONF.System.BaseDir, oldAppBackupPath)) - dropFileOrDir(path.Join(global.CONF.System.BaseDir, oldDownloadPath)) - files, _ := os.ReadDir(path.Join(global.CONF.System.BaseDir, oldUpgradePath)) - if len(files) == 0 { - continue - } - for _, file := range files { - if strings.HasPrefix(file.Name(), "upgrade_") { - dropFileOrDir(path.Join(global.CONF.System.BaseDir, oldUpgradePath, file.Name())) - } - } - case "old_original": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, oldOriginalPath, item.Name)) - case "old_apps_bak": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, oldAppBackupPath, item.Name)) - case "old_download": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, oldDownloadPath, item.Name)) - case "old_upgrade": - if len(item.Name) == 0 { - files, _ := os.ReadDir(path.Join(global.CONF.System.BaseDir, oldUpgradePath)) - if len(files) == 0 { - continue - } - for _, file := range files { - if strings.HasPrefix(file.Name(), "upgrade_") { - dropFileOrDir(path.Join(global.CONF.System.BaseDir, oldUpgradePath, file.Name())) - } - } - } else { - dropFileOrDir(path.Join(global.CONF.System.BaseDir, oldUpgradePath, item.Name)) - } - - case "upload": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, uploadPath, item.Name)) - if len(item.Name) == 0 { - dropFileOrDir(path.Join(global.CONF.System.BaseDir, tmpUploadPath)) - } - case "upload_tmp": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, tmpUploadPath, item.Name)) - case "upload_app": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, uploadPath, "app", item.Name)) - case "upload_database": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, uploadPath, "database", item.Name)) - case "upload_website": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, uploadPath, "website", item.Name)) - case "upload_directory": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, uploadPath, "directory", item.Name)) - case "download": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, downloadPath, item.Name)) - case "download_app": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, downloadPath, "app", item.Name)) - case "download_database": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, downloadPath, "database", item.Name)) - case "download_website": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, downloadPath, "website", item.Name)) - case "download_directory": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, downloadPath, "directory", item.Name)) - - case "system_log": - if len(item.Name) == 0 { - files, _ := os.ReadDir(path.Join(global.CONF.System.BaseDir, logPath)) - if len(files) == 0 { - continue - } - for _, file := range files { - if file.Name() == "1Panel.log" || file.IsDir() { - continue - } - dropFileOrDir(path.Join(global.CONF.System.BaseDir, logPath, file.Name())) - } - } else { - dropFileOrDir(path.Join(global.CONF.System.BaseDir, logPath, item.Name)) - } - case "docker_log": - dropFileOrDir(path.Join(global.CONF.System.BaseDir, dockerLogPath, item.Name)) - case "task_log": - pathItem := path.Join(global.CONF.System.BaseDir, taskPath, item.Name) - dropFileOrDir(path.Join(global.CONF.System.BaseDir, taskPath, item.Name)) - if len(item.Name) == 0 { - files, _ := os.ReadDir(pathItem) - if len(files) == 0 { - continue - } - for _, file := range files { - _ = cronjobRepo.DeleteRecord(cronjobRepo.WithByRecordFile(path.Join(pathItem, file.Name()))) - } - } else { - _ = cronjobRepo.DeleteRecord(cronjobRepo.WithByRecordFile(pathItem)) - } - case "images": - dropImages() - case "containers": - dropContainers() - case "volumes": - dropVolumes() - case "build_cache": - dropBuildCache() - } - } - - _ = settingRepo.Update("LastCleanTime", time.Now().Format(constant.DateTimeLayout)) - _ = settingRepo.Update("LastCleanSize", fmt.Sprintf("%v", size)) - _ = settingRepo.Update("LastCleanData", fmt.Sprintf("%v", len(req))) - - if restart { - go func() { - _, err := cmd.Exec("systemctl restart 1panel.service") - if err != nil { - global.LOG.Errorf("restart system port failed, err: %v", err) - } - }() - } -} - -func (u *DeviceService) CleanForCronjob() (string, error) { - logs := "" - size := int64(0) - fileCount := 0 - dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, "1panel_original"), &logs, &size, &fileCount) - - upgradePath := path.Join(global.CONF.System.BaseDir, upgradePath) - upgradeFiles, _ := os.ReadDir(upgradePath) - if len(upgradeFiles) != 0 { - sort.Slice(upgradeFiles, func(i, j int) bool { - return upgradeFiles[i].Name() > upgradeFiles[j].Name() - }) - for i := 0; i < len(upgradeFiles); i++ { - if i != 0 { - dropFileOrDirWithLog(path.Join(upgradePath, upgradeFiles[i].Name()), &logs, &size, &fileCount) - } - } - } - - dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, snapshotTmpPath), &logs, &size, &fileCount) - dropFileOrDirWithLog(path.Join(global.CONF.System.Backup, "system"), &logs, &size, &fileCount) - - dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, rollbackPath, "app"), &logs, &size, &fileCount) - dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, rollbackPath, "website"), &logs, &size, &fileCount) - dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, rollbackPath, "database"), &logs, &size, &fileCount) - - dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, oldOriginalPath), &logs, &size, &fileCount) - dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, oldAppBackupPath), &logs, &size, &fileCount) - dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, oldDownloadPath), &logs, &size, &fileCount) - oldUpgradePath := path.Join(global.CONF.System.BaseDir, oldUpgradePath) - oldUpgradeFiles, _ := os.ReadDir(oldUpgradePath) - if len(oldUpgradeFiles) != 0 { - for i := 0; i < len(oldUpgradeFiles); i++ { - dropFileOrDirWithLog(path.Join(oldUpgradePath, oldUpgradeFiles[i].Name()), &logs, &size, &fileCount) - } - } - - dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, tmpUploadPath), &logs, &size, &fileCount) - dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, uploadPath), &logs, &size, &fileCount) - dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, downloadPath), &logs, &size, &fileCount) - - logPath := path.Join(global.CONF.System.BaseDir, logPath) - logFiles, _ := os.ReadDir(logPath) - if len(logFiles) != 0 { - for i := 0; i < len(logFiles); i++ { - if logFiles[i].Name() != "1Panel.log" { - dropFileOrDirWithLog(path.Join(logPath, logFiles[i].Name()), &logs, &size, &fileCount) - } - } - } - timeNow := time.Now().Format(constant.DateTimeLayout) - dropFileOrDirWithLog(path.Join(global.CONF.System.BaseDir, dockerLogPath), &logs, &size, &fileCount) - logs += fmt.Sprintf("\n%s: total clean: %s, total count: %d", timeNow, common.LoadSizeUnit2F(float64(size)), fileCount) - - _ = settingRepo.Update("LastCleanTime", timeNow) - _ = settingRepo.Update("LastCleanSize", fmt.Sprintf("%v", size)) - _ = settingRepo.Update("LastCleanData", fmt.Sprintf("%v", fileCount)) - - return logs, nil -} - -func loadSnapshotTree(fileOp fileUtils.FileOp) []dto.CleanTree { - var treeData []dto.CleanTree - path1 := path.Join(global.CONF.System.BaseDir, snapshotTmpPath) - list1 := loadTreeWithAllFile(true, path1, "snapshot_tmp", path1, fileOp) - if len(list1) != 0 { - size, _ := fileOp.GetDirSize(path1) - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "snapshot_tmp", Size: uint64(size), Children: list1, Type: "snapshot_tmp", IsRecommend: true}) - } - - path2 := path.Join(global.CONF.System.Backup, "system") - list2 := loadTreeWithAllFile(true, path2, "snapshot_local", path2, fileOp) - if len(list2) != 0 { - size, _ := fileOp.GetDirSize(path2) - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "snapshot_local", Size: uint64(size), Children: list2, Type: "snapshot_local", IsRecommend: true}) - } - return treeData -} - -func loadRollBackTree(fileOp fileUtils.FileOp) []dto.CleanTree { - var treeData []dto.CleanTree - path1 := path.Join(global.CONF.System.BaseDir, rollbackPath, "app") - list1 := loadTreeWithAllFile(true, path1, "rollback_app", path1, fileOp) - size1, _ := fileOp.GetDirSize(path1) - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "rollback_app", Size: uint64(size1), Children: list1, Type: "rollback_app", IsRecommend: true}) - - path2 := path.Join(global.CONF.System.BaseDir, rollbackPath, "website") - list2 := loadTreeWithAllFile(true, path2, "rollback_website", path2, fileOp) - size2, _ := fileOp.GetDirSize(path2) - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "rollback_website", Size: uint64(size2), Children: list2, Type: "rollback_website", IsRecommend: true}) - - path3 := path.Join(global.CONF.System.BaseDir, rollbackPath, "database") - list3 := loadTreeWithAllFile(true, path3, "rollback_database", path3, fileOp) - size3, _ := fileOp.GetDirSize(path3) - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "rollback_database", Size: uint64(size3), Children: list3, Type: "rollback_database", IsRecommend: true}) - - return treeData -} - -func loadUnusedFile(fileOp fileUtils.FileOp) []dto.CleanTree { - var treeData []dto.CleanTree - path1 := path.Join(global.CONF.System.BaseDir, oldOriginalPath) - list1 := loadTreeWithAllFile(true, path1, "old_original", path1, fileOp) - if len(list1) != 0 { - size, _ := fileOp.GetDirSize(path1) - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "old_original", Size: uint64(size), Children: list1, Type: "old_original"}) - } - - path2 := path.Join(global.CONF.System.BaseDir, oldAppBackupPath) - list2 := loadTreeWithAllFile(true, path2, "old_apps_bak", path2, fileOp) - if len(list2) != 0 { - size, _ := fileOp.GetDirSize(path2) - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "old_apps_bak", Size: uint64(size), Children: list2, Type: "old_apps_bak"}) - } - - path3 := path.Join(global.CONF.System.BaseDir, oldDownloadPath) - list3 := loadTreeWithAllFile(true, path3, "old_download", path3, fileOp) - if len(list3) != 0 { - size, _ := fileOp.GetDirSize(path3) - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "old_download", Size: uint64(size), Children: list3, Type: "old_download"}) - } - - path4 := path.Join(global.CONF.System.BaseDir, oldUpgradePath) - list4 := loadTreeWithDir(true, "old_upgrade", path4, fileOp) - itemSize := uint64(0) - for _, item := range list4 { - itemSize += item.Size - } - if len(list4) != 0 { - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "old_upgrade", Size: itemSize, Children: list4, Type: "old_upgrade"}) - } - return treeData -} - -func loadUploadTree(fileOp fileUtils.FileOp) []dto.CleanTree { - var treeData []dto.CleanTree - - path0 := path.Join(global.CONF.System.BaseDir, tmpUploadPath) - list0 := loadTreeWithAllFile(true, path0, "upload_tmp", path0, fileOp) - size0, _ := fileOp.GetDirSize(path0) - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "upload_tmp", Size: uint64(size0), Children: list0, Type: "upload_tmp", IsRecommend: true}) - - path1 := path.Join(global.CONF.System.BaseDir, uploadPath, "app") - list1 := loadTreeWithAllFile(true, path1, "upload_app", path1, fileOp) - size1, _ := fileOp.GetDirSize(path1) - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "upload_app", Size: uint64(size1), Children: list1, Type: "upload_app", IsRecommend: true}) - - path2 := path.Join(global.CONF.System.BaseDir, uploadPath, "website") - list2 := loadTreeWithAllFile(true, path2, "upload_website", path2, fileOp) - size2, _ := fileOp.GetDirSize(path2) - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "upload_website", Size: uint64(size2), Children: list2, Type: "upload_website", IsRecommend: true}) - - path3 := path.Join(global.CONF.System.BaseDir, uploadPath, "database") - list3 := loadTreeWithAllFile(true, path3, "upload_database", path3, fileOp) - size3, _ := fileOp.GetDirSize(path3) - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "upload_database", Size: uint64(size3), Children: list3, Type: "upload_database", IsRecommend: true}) - - path4 := path.Join(global.CONF.System.BaseDir, uploadPath, "directory") - list4 := loadTreeWithAllFile(true, path4, "upload_directory", path4, fileOp) - size4, _ := fileOp.GetDirSize(path4) - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "upload_directory", Size: uint64(size4), Children: list4, Type: "upload_directory", IsRecommend: true}) - - path5 := path.Join(global.CONF.System.BaseDir, uploadPath) - uploadTreeData := loadTreeWithAllFile(true, path5, "upload", path5, fileOp) - treeData = append(treeData, uploadTreeData...) - - return treeData -} - -func loadDownloadTree(fileOp fileUtils.FileOp) []dto.CleanTree { - var treeData []dto.CleanTree - path1 := path.Join(global.CONF.System.BaseDir, downloadPath, "app") - list1 := loadTreeWithAllFile(true, path1, "download_app", path1, fileOp) - size1, _ := fileOp.GetDirSize(path1) - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "download_app", Size: uint64(size1), Children: list1, Type: "download_app", IsRecommend: true}) - - path2 := path.Join(global.CONF.System.BaseDir, downloadPath, "website") - list2 := loadTreeWithAllFile(true, path2, "download_website", path2, fileOp) - size2, _ := fileOp.GetDirSize(path2) - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "download_website", Size: uint64(size2), Children: list2, Type: "download_website", IsRecommend: true}) - - path3 := path.Join(global.CONF.System.BaseDir, downloadPath, "database") - list3 := loadTreeWithAllFile(true, path3, "download_database", path3, fileOp) - size3, _ := fileOp.GetDirSize(path3) - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "download_database", Size: uint64(size3), Children: list3, Type: "download_database", IsRecommend: true}) - - path4 := path.Join(global.CONF.System.BaseDir, downloadPath, "directory") - list4 := loadTreeWithAllFile(true, path4, "download_directory", path4, fileOp) - size4, _ := fileOp.GetDirSize(path4) - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "download_directory", Size: uint64(size4), Children: list4, Type: "download_directory", IsRecommend: true}) - - path5 := path.Join(global.CONF.System.BaseDir, downloadPath) - uploadTreeData := loadTreeWithAllFile(true, path5, "download", path5, fileOp) - treeData = append(treeData, uploadTreeData...) - - return treeData -} - -func loadLogTree(fileOp fileUtils.FileOp) []dto.CleanTree { - var treeData []dto.CleanTree - path1 := path.Join(global.CONF.System.BaseDir, logPath) - list1 := loadTreeWithAllFile(true, path1, "system_log", path1, fileOp) - size := uint64(0) - for _, file := range list1 { - size += file.Size - } - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "system_log", Size: uint64(size), Children: list1, Type: "system_log", IsRecommend: true}) - - path2 := path.Join(global.CONF.System.BaseDir, dockerLogPath) - list2 := loadTreeWithAllFile(true, path2, "docker_log", path2, fileOp) - size2, _ := fileOp.GetDirSize(path2) - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "docker_log", Size: uint64(size2), Children: list2, Type: "docker_log", IsRecommend: true}) - - path3 := path.Join(global.CONF.System.BaseDir, taskPath) - list3 := loadTreeWithAllFile(false, path3, "task_log", path3, fileOp) - size3, _ := fileOp.GetDirSize(path3) - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "task_log", Size: uint64(size3), Children: list3, Type: "task_log"}) - return treeData -} - -func loadContainerTree() []dto.CleanTree { - var treeData []dto.CleanTree - client, err := docker.NewDockerClient() - if err != nil { - return treeData - } - diskUsage, err := client.DiskUsage(context.Background(), types.DiskUsageOptions{}) - if err != nil { - return treeData - } - imageSize := uint64(0) - for _, file := range diskUsage.Images { - if file.Containers == 0 { - imageSize += uint64(file.Size) - } - } - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_images", Size: imageSize, Children: nil, Type: "images", IsRecommend: true}) - - containerSize := uint64(0) - for _, file := range diskUsage.Containers { - if file.State != "running" { - containerSize += uint64(file.SizeRw) - } - } - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_containers", Size: containerSize, Children: nil, Type: "containers", IsRecommend: true}) - - volumeSize := uint64(0) - for _, file := range diskUsage.Volumes { - if file.UsageData.RefCount <= 0 { - volumeSize += uint64(file.UsageData.Size) - } - } - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_volumes", Size: volumeSize, Children: nil, Type: "volumes", IsRecommend: true}) - - var buildCacheTotalSize int64 - for _, cache := range diskUsage.BuildCache { - if cache.Type == "source.local" { - buildCacheTotalSize += cache.Size - } - } - treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "build_cache", Size: uint64(buildCacheTotalSize), Type: "build_cache", IsRecommend: true}) - return treeData -} - -func loadTreeWithDir(isCheck bool, treeType, pathItem string, fileOp fileUtils.FileOp) []dto.CleanTree { - var lists []dto.CleanTree - files, err := os.ReadDir(pathItem) - if err != nil { - return lists - } - sort.Slice(files, func(i, j int) bool { - return files[i].Name() > files[j].Name() - }) - for _, file := range files { - if (treeType == "old_upgrade" || treeType == "upgrade") && !strings.HasPrefix(file.Name(), "upgrade_2023") { - continue - } - if file.IsDir() { - size, err := fileOp.GetDirSize(path.Join(pathItem, file.Name())) - if err != nil { - continue - } - item := dto.CleanTree{ - ID: uuid.NewString(), - Label: file.Name(), - Type: treeType, - Size: uint64(size), - Name: strings.TrimPrefix(file.Name(), "/"), - IsCheck: isCheck, - IsRecommend: isCheck, - } - if treeType == "upgrade" && len(lists) == 0 { - item.IsCheck = false - item.IsRecommend = false - } - lists = append(lists, item) - } - } - return lists -} - -func loadTreeWithAllFile(isCheck bool, originalPath, treeType, pathItem string, fileOp fileUtils.FileOp) []dto.CleanTree { - var lists []dto.CleanTree - - files, err := os.ReadDir(pathItem) - if err != nil { - return lists - } - for _, file := range files { - if treeType == "system_log" && (file.Name() == "1Panel.log" || file.IsDir()) { - continue - } - if (treeType == "upload" || treeType == "download") && file.IsDir() && (file.Name() == "app" || file.Name() == "database" || file.Name() == "website" || file.Name() == "directory") { - continue - } - size := uint64(0) - name := strings.TrimPrefix(path.Join(pathItem, file.Name()), originalPath+"/") - if file.IsDir() { - sizeItem, err := fileOp.GetDirSize(path.Join(pathItem, file.Name())) - if err != nil { - continue - } - size = uint64(sizeItem) - } else { - fileInfo, err := file.Info() - if err != nil { - continue - } - size = uint64(fileInfo.Size()) - } - item := dto.CleanTree{ - ID: uuid.NewString(), - Label: file.Name(), - Type: treeType, - Size: uint64(size), - Name: name, - IsCheck: isCheck, - IsRecommend: isCheck, - } - if file.IsDir() { - item.Children = loadTreeWithAllFile(isCheck, originalPath, treeType, path.Join(pathItem, file.Name()), fileOp) - } - lists = append(lists, item) - } - return lists -} - -func dropFileOrDir(itemPath string) { - global.LOG.Debugf("drop file %s", itemPath) - if err := os.RemoveAll(itemPath); err != nil { - global.LOG.Errorf("drop file %s failed, err %v", itemPath, err) - } -} - -func dropBuildCache() { - client, err := docker.NewDockerClient() - if err != nil { - global.LOG.Errorf("do not get docker client") - } - opts := types.BuildCachePruneOptions{} - opts.All = true - _, err = client.BuildCachePrune(context.Background(), opts) - if err != nil { - global.LOG.Errorf("drop build cache failed, err %v", err) - } -} - -func dropImages() { - client, err := docker.NewDockerClient() - if err != nil { - global.LOG.Errorf("do not get docker client") - } - pruneFilters := filters.NewArgs() - pruneFilters.Add("dangling", "false") - _, err = client.ImagesPrune(context.Background(), pruneFilters) - if err != nil { - global.LOG.Errorf("drop images failed, err %v", err) - } -} - -func dropContainers() { - client, err := docker.NewDockerClient() - if err != nil { - global.LOG.Errorf("do not get docker client") - } - pruneFilters := filters.NewArgs() - _, err = client.ContainersPrune(context.Background(), pruneFilters) - if err != nil { - global.LOG.Errorf("drop containers failed, err %v", err) - } -} - -func dropVolumes() { - client, err := docker.NewDockerClient() - if err != nil { - global.LOG.Errorf("do not get docker client") - } - pruneFilters := filters.NewArgs() - versions, err := client.ServerVersion(context.Background()) - if err != nil { - global.LOG.Errorf("do not get docker api versions") - } - if common.ComparePanelVersion(versions.APIVersion, "1.42") { - pruneFilters.Add("all", "true") - } - _, err = client.VolumesPrune(context.Background(), pruneFilters) - if err != nil { - global.LOG.Errorf("drop volumes failed, err %v", err) - } -} - -func dropFileOrDirWithLog(itemPath string, log *string, size *int64, count *int) { - itemSize := int64(0) - itemCount := 0 - scanFile(itemPath, &itemSize, &itemCount) - *size += itemSize - *count += itemCount - if err := os.RemoveAll(itemPath); err != nil { - global.LOG.Errorf("drop file %s failed, err %v", itemPath, err) - *log += fmt.Sprintf("- drop file %s failed, err: %v \n\n", itemPath, err) - return - } - *log += fmt.Sprintf("+ drop file %s successful!, size: %s, count: %d \n\n", itemPath, common.LoadSizeUnit2F(float64(itemSize)), itemCount) -} - -func scanFile(pathItem string, size *int64, count *int) { - files, _ := os.ReadDir(pathItem) - for _, f := range files { - if f.IsDir() { - scanFile(path.Join(pathItem, f.Name()), size, count) - } else { - fileInfo, err := f.Info() - if err != nil { - continue - } - *count++ - *size += fileInfo.Size() - } - } -} diff --git a/backend/app/service/docker.go b/backend/app/service/docker.go deleted file mode 100644 index 21418f4d1..000000000 --- a/backend/app/service/docker.go +++ /dev/null @@ -1,388 +0,0 @@ -package service - -import ( - "bufio" - "context" - "encoding/json" - "os" - "path" - "strings" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/docker" - "github.com/pkg/errors" -) - -type DockerService struct{} - -type IDockerService interface { - UpdateConf(req dto.SettingUpdate) error - UpdateLogOption(req dto.LogOption) error - UpdateIpv6Option(req dto.Ipv6Option) error - UpdateConfByFile(info dto.DaemonJsonUpdateByFile) error - LoadDockerStatus() string - LoadDockerConf() *dto.DaemonJsonConf - OperateDocker(req dto.DockerOperation) error -} - -func NewIDockerService() IDockerService { - return &DockerService{} -} - -type daemonJsonItem struct { - Status string `json:"status"` - Mirrors []string `json:"registry-mirrors"` - Registries []string `json:"insecure-registries"` - LiveRestore bool `json:"live-restore"` - Ipv6 bool `json:"ipv6"` - FixedCidrV6 string `json:"fixed-cidr-v6"` - Ip6Tables bool `json:"ip6tables"` - Experimental bool `json:"experimental"` - IPTables bool `json:"iptables"` - ExecOpts []string `json:"exec-opts"` - LogOption logOption `json:"log-opts"` -} -type logOption struct { - LogMaxSize string `json:"max-size"` - LogMaxFile string `json:"max-file"` -} - -func (u *DockerService) LoadDockerStatus() string { - client, err := docker.NewDockerClient() - if err != nil { - return constant.Stopped - } - defer client.Close() - if _, err := client.Ping(context.Background()); err != nil { - return constant.Stopped - } - - return constant.StatusRunning -} - -func (u *DockerService) LoadDockerConf() *dto.DaemonJsonConf { - ctx := context.Background() - var data dto.DaemonJsonConf - data.IPTables = true - data.Status = constant.StatusRunning - data.Version = "-" - client, err := docker.NewDockerClient() - if err != nil { - data.Status = constant.Stopped - } else { - defer client.Close() - if _, err := client.Ping(ctx); err != nil { - data.Status = constant.Stopped - } - itemVersion, err := client.ServerVersion(ctx) - if err == nil { - data.Version = itemVersion.Version - } - } - data.IsSwarm = false - stdout2, _ := cmd.Exec("docker info | grep Swarm") - if string(stdout2) == " Swarm: active\n" { - data.IsSwarm = true - } - if _, err := os.Stat(constant.DaemonJsonPath); err != nil { - return &data - } - file, err := os.ReadFile(constant.DaemonJsonPath) - if err != nil { - return &data - } - var conf daemonJsonItem - daemonMap := make(map[string]interface{}) - if err := json.Unmarshal(file, &daemonMap); err != nil { - return &data - } - arr, err := json.Marshal(daemonMap) - if err != nil { - return &data - } - if err := json.Unmarshal(arr, &conf); err != nil { - return &data - } - if _, ok := daemonMap["iptables"]; !ok { - conf.IPTables = true - } - data.CgroupDriver = "cgroupfs" - for _, opt := range conf.ExecOpts { - if strings.HasPrefix(opt, "native.cgroupdriver=") { - data.CgroupDriver = strings.ReplaceAll(opt, "native.cgroupdriver=", "") - break - } - } - data.Ipv6 = conf.Ipv6 - data.FixedCidrV6 = conf.FixedCidrV6 - data.Ip6Tables = conf.Ip6Tables - data.Experimental = conf.Experimental - data.LogMaxSize = conf.LogOption.LogMaxSize - data.LogMaxFile = conf.LogOption.LogMaxFile - data.Mirrors = conf.Mirrors - data.Registries = conf.Registries - data.IPTables = conf.IPTables - data.LiveRestore = conf.LiveRestore - return &data -} - -func (u *DockerService) UpdateConf(req dto.SettingUpdate) error { - err := createIfNotExistDaemonJsonFile() - if err != nil { - return err - } - file, err := os.ReadFile(constant.DaemonJsonPath) - if err != nil { - return err - } - daemonMap := make(map[string]interface{}) - _ = json.Unmarshal(file, &daemonMap) - - switch req.Key { - case "Registries": - req.Value = strings.TrimSuffix(req.Value, ",") - if len(req.Value) == 0 { - delete(daemonMap, "insecure-registries") - } else { - daemonMap["insecure-registries"] = strings.Split(req.Value, ",") - } - case "Mirrors": - req.Value = strings.TrimSuffix(req.Value, ",") - if len(req.Value) == 0 { - delete(daemonMap, "registry-mirrors") - } else { - daemonMap["registry-mirrors"] = strings.Split(req.Value, ",") - } - case "Ipv6": - if req.Value == "disable" { - delete(daemonMap, "ipv6") - delete(daemonMap, "fixed-cidr-v6") - delete(daemonMap, "ip6tables") - delete(daemonMap, "experimental") - } - case "LogOption": - if req.Value == "disable" { - delete(daemonMap, "log-opts") - } - case "LiveRestore": - if req.Value == "disable" { - delete(daemonMap, "live-restore") - } else { - daemonMap["live-restore"] = true - } - case "IPtables": - if req.Value == "enable" { - delete(daemonMap, "iptables") - } else { - daemonMap["iptables"] = false - } - case "Driver": - if opts, ok := daemonMap["exec-opts"]; ok { - if optsValue, isArray := opts.([]interface{}); isArray { - for i := 0; i < len(optsValue); i++ { - if opt, isStr := optsValue[i].(string); isStr { - if strings.HasPrefix(opt, "native.cgroupdriver=") { - optsValue[i] = "native.cgroupdriver=" + req.Value - break - } - } - } - } - } else { - if req.Value == "systemd" { - daemonMap["exec-opts"] = []string{"native.cgroupdriver=systemd"} - } - } - } - if len(daemonMap) == 0 { - _ = os.Remove(constant.DaemonJsonPath) - return nil - } - newJson, err := json.MarshalIndent(daemonMap, "", "\t") - if err != nil { - return err - } - if err := os.WriteFile(constant.DaemonJsonPath, newJson, 0640); err != nil { - return err - } - - stdout, err := cmd.Exec("systemctl restart docker") - if err != nil { - return errors.New(string(stdout)) - } - return nil -} -func createIfNotExistDaemonJsonFile() error { - if _, err := os.Stat(constant.DaemonJsonPath); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(path.Dir(constant.DaemonJsonPath), os.ModePerm); err != nil { - return err - } - var daemonFile *os.File - daemonFile, err = os.Create(constant.DaemonJsonPath) - if err != nil { - return err - } - defer daemonFile.Close() - } - return nil -} - -func (u *DockerService) UpdateLogOption(req dto.LogOption) error { - err := createIfNotExistDaemonJsonFile() - if err != nil { - return err - } - file, err := os.ReadFile(constant.DaemonJsonPath) - if err != nil { - return err - } - daemonMap := make(map[string]interface{}) - _ = json.Unmarshal(file, &daemonMap) - - changeLogOption(daemonMap, req.LogMaxFile, req.LogMaxSize) - if len(daemonMap) == 0 { - _ = os.Remove(constant.DaemonJsonPath) - return nil - } - newJson, err := json.MarshalIndent(daemonMap, "", "\t") - if err != nil { - return err - } - if err := os.WriteFile(constant.DaemonJsonPath, newJson, 0640); err != nil { - return err - } - - stdout, err := cmd.Exec("systemctl restart docker") - if err != nil { - return errors.New(string(stdout)) - } - return nil -} - -func (u *DockerService) UpdateIpv6Option(req dto.Ipv6Option) error { - err := createIfNotExistDaemonJsonFile() - if err != nil { - return err - } - - file, err := os.ReadFile(constant.DaemonJsonPath) - if err != nil { - return err - } - daemonMap := make(map[string]interface{}) - _ = json.Unmarshal(file, &daemonMap) - - daemonMap["ipv6"] = true - daemonMap["fixed-cidr-v6"] = req.FixedCidrV6 - if req.Ip6Tables { - daemonMap["ip6tables"] = req.Ip6Tables - } - if req.Experimental { - daemonMap["experimental"] = req.Experimental - } - if len(daemonMap) == 0 { - _ = os.Remove(constant.DaemonJsonPath) - return nil - } - newJson, err := json.MarshalIndent(daemonMap, "", "\t") - if err != nil { - return err - } - if err := os.WriteFile(constant.DaemonJsonPath, newJson, 0640); err != nil { - return err - } - - stdout, err := cmd.Exec("systemctl restart docker") - if err != nil { - return errors.New(string(stdout)) - } - return nil -} - -func (u *DockerService) UpdateConfByFile(req dto.DaemonJsonUpdateByFile) error { - if len(req.File) == 0 { - _ = os.Remove(constant.DaemonJsonPath) - return nil - } - err := createIfNotExistDaemonJsonFile() - if err != nil { - return err - } - file, err := os.OpenFile(constant.DaemonJsonPath, os.O_WRONLY|os.O_TRUNC, 0640) - if err != nil { - return err - } - defer file.Close() - write := bufio.NewWriter(file) - _, _ = write.WriteString(req.File) - write.Flush() - - stdout, err := cmd.Exec("systemctl restart docker") - if err != nil { - return errors.New(string(stdout)) - } - return nil -} - -func (u *DockerService) OperateDocker(req dto.DockerOperation) error { - service := "docker" - if req.Operation == "stop" { - service = "docker.socket" - } - stdout, err := cmd.Execf("systemctl %s %s ", req.Operation, service) - if err != nil { - return errors.New(string(stdout)) - } - return nil -} - -func changeLogOption(daemonMap map[string]interface{}, logMaxFile, logMaxSize string) { - if opts, ok := daemonMap["log-opts"]; ok { - if len(logMaxFile) != 0 || len(logMaxSize) != 0 { - daemonMap["log-driver"] = "json-file" - } - optsMap, isMap := opts.(map[string]interface{}) - if isMap { - if len(logMaxFile) != 0 { - optsMap["max-file"] = logMaxFile - } else { - delete(optsMap, "max-file") - } - if len(logMaxSize) != 0 { - optsMap["max-size"] = logMaxSize - } else { - delete(optsMap, "max-size") - } - if len(optsMap) == 0 { - delete(daemonMap, "log-opts") - } - } else { - optsMap := make(map[string]interface{}) - if len(logMaxFile) != 0 { - optsMap["max-file"] = logMaxFile - } - if len(logMaxSize) != 0 { - optsMap["max-size"] = logMaxSize - } - if len(optsMap) != 0 { - daemonMap["log-opts"] = optsMap - } - } - } else { - if len(logMaxFile) != 0 || len(logMaxSize) != 0 { - daemonMap["log-driver"] = "json-file" - } - optsMap := make(map[string]interface{}) - if len(logMaxFile) != 0 { - optsMap["max-file"] = logMaxFile - } - if len(logMaxSize) != 0 { - optsMap["max-size"] = logMaxSize - } - if len(optsMap) != 0 { - daemonMap["log-opts"] = optsMap - } - } -} diff --git a/backend/app/service/entry.go b/backend/app/service/entry.go deleted file mode 100644 index 59c1d6d42..000000000 --- a/backend/app/service/entry.go +++ /dev/null @@ -1,46 +0,0 @@ -package service - -import "github.com/1Panel-dev/1Panel/backend/app/repo" - -var ( - commonRepo = repo.NewCommonRepo() - - appRepo = repo.NewIAppRepo() - appTagRepo = repo.NewIAppTagRepo() - appDetailRepo = repo.NewIAppDetailRepo() - tagRepo = repo.NewITagRepo() - appInstallRepo = repo.NewIAppInstallRepo() - appInstallResourceRepo = repo.NewIAppInstallResourceRpo() - - mysqlRepo = repo.NewIMysqlRepo() - postgresqlRepo = repo.NewIPostgresqlRepo() - databaseRepo = repo.NewIDatabaseRepo() - - imageRepoRepo = repo.NewIImageRepoRepo() - composeRepo = repo.NewIComposeTemplateRepo() - - cronjobRepo = repo.NewICronjobRepo() - - hostRepo = repo.NewIHostRepo() - groupRepo = repo.NewIGroupRepo() - commandRepo = repo.NewICommandRepo() - ftpRepo = repo.NewIFtpRepo() - clamRepo = repo.NewIClamRepo() - - settingRepo = repo.NewISettingRepo() - backupRepo = repo.NewIBackupRepo() - - websiteRepo = repo.NewIWebsiteRepo() - websiteDomainRepo = repo.NewIWebsiteDomainRepo() - websiteDnsRepo = repo.NewIWebsiteDnsAccountRepo() - websiteSSLRepo = repo.NewISSLRepo() - websiteAcmeRepo = repo.NewIAcmeAccountRepo() - websiteCARepo = repo.NewIWebsiteCARepo() - - snapshotRepo = repo.NewISnapshotRepo() - - runtimeRepo = repo.NewIRunTimeRepo() - phpExtensionsRepo = repo.NewIPHPExtensionsRepo() - - favoriteRepo = repo.NewIFavoriteRepo() -) diff --git a/backend/app/service/fail2ban.go b/backend/app/service/fail2ban.go deleted file mode 100644 index a881cd66a..000000000 --- a/backend/app/service/fail2ban.go +++ /dev/null @@ -1,251 +0,0 @@ -package service - -import ( - "bufio" - "fmt" - "os" - "strconv" - "strings" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/utils/firewall" - "github.com/1Panel-dev/1Panel/backend/utils/toolbox" -) - -const defaultFail2BanPath = "/etc/fail2ban/jail.local" - -type Fail2BanService struct{} - -type IFail2BanService interface { - LoadBaseInfo() (dto.Fail2BanBaseInfo, error) - Search(search dto.Fail2BanSearch) ([]string, error) - Operate(operation string) error - OperateSSHD(req dto.Fail2BanSet) error - UpdateConf(req dto.Fail2BanUpdate) error - UpdateConfByFile(req dto.UpdateByFile) error -} - -func NewIFail2BanService() IFail2BanService { - return &Fail2BanService{} -} - -func (u *Fail2BanService) LoadBaseInfo() (dto.Fail2BanBaseInfo, error) { - var baseInfo dto.Fail2BanBaseInfo - client, err := toolbox.NewFail2Ban() - if err != nil { - return baseInfo, err - } - baseInfo.IsEnable, baseInfo.IsActive, baseInfo.IsExist = client.Status() - if !baseInfo.IsActive { - baseInfo.Version = "-" - } else { - baseInfo.Version = client.Version() - } - conf, err := os.ReadFile(defaultFail2BanPath) - if err != nil { - if baseInfo.IsActive { - return baseInfo, fmt.Errorf("read fail2ban conf of %s failed, err: %v", defaultFail2BanPath, err) - } else { - return baseInfo, nil - } - } - lines := strings.Split(string(conf), "\n") - - block := "" - for _, line := range lines { - if strings.HasPrefix(strings.ToLower(line), "[default]") { - block = "default" - continue - } - if strings.HasPrefix(line, "[sshd]") { - block = "sshd" - continue - } - if strings.HasPrefix(line, "[") { - block = "" - continue - } - if block != "default" && block != "sshd" { - continue - } - loadFailValue(line, &baseInfo) - } - - return baseInfo, nil -} - -func (u *Fail2BanService) Search(req dto.Fail2BanSearch) ([]string, error) { - var list []string - client, err := toolbox.NewFail2Ban() - if err != nil { - return nil, err - } - if req.Status == "banned" { - list, err = client.ListBanned() - - } else { - list, err = client.ListIgnore() - } - if err != nil { - return nil, err - } - - return list, nil -} - -func (u *Fail2BanService) Operate(operation string) error { - client, err := toolbox.NewFail2Ban() - if err != nil { - return err - } - return client.Operate(operation) -} - -func (u *Fail2BanService) UpdateConf(req dto.Fail2BanUpdate) error { - if req.Key == "banaction" { - if req.Value == "firewallcmd-ipset" || req.Value == "ufw" { - itemName := "ufw" - if req.Value == "firewallcmd-ipset" { - itemName = "firewalld" - } - client, err := firewall.NewFirewallClient() - if err != nil { - return err - } - if client.Name() != itemName { - return buserr.WithName("ErrBanAction", itemName) - } - status, _ := client.Status() - if status != "running" { - return buserr.WithName("ErrBanAction", itemName) - } - } - } - if req.Key == "logpath" { - if _, err := os.Stat(req.Value); err != nil { - return err - } - } - conf, err := os.ReadFile(defaultFail2BanPath) - if err != nil { - return fmt.Errorf("read fail2ban conf of %s failed, err: %v", defaultFail2BanPath, err) - } - lines := strings.Split(string(conf), "\n") - - isStart, isEnd, hasKey := false, false, false - newFile := "" - for index, line := range lines { - if !isStart && strings.HasPrefix(line, "[sshd]") { - isStart = true - newFile += fmt.Sprintf("%s\n", line) - continue - } - if !isStart || isEnd { - newFile += fmt.Sprintf("%s\n", line) - continue - } - if strings.HasPrefix(line, req.Key) { - hasKey = true - newFile += fmt.Sprintf("%s = %s\n", req.Key, req.Value) - continue - } - if strings.HasPrefix(line, "[") || index == len(lines)-1 { - isEnd = true - if !hasKey { - newFile += fmt.Sprintf("%s = %s\n", req.Key, req.Value) - } - } - newFile += line - if index != len(lines)-1 { - newFile += "\n" - } - } - file, err := os.OpenFile(defaultFail2BanPath, os.O_WRONLY|os.O_TRUNC, 0640) - if err != nil { - return err - } - defer file.Close() - write := bufio.NewWriter(file) - _, _ = write.WriteString(newFile) - write.Flush() - - client, err := toolbox.NewFail2Ban() - if err != nil { - return err - } - if err := client.Operate("restart"); err != nil { - return err - } - return nil -} - -func (u *Fail2BanService) UpdateConfByFile(req dto.UpdateByFile) error { - file, err := os.OpenFile(defaultFail2BanPath, os.O_WRONLY|os.O_TRUNC, 0640) - if err != nil { - return err - } - defer file.Close() - write := bufio.NewWriter(file) - _, _ = write.WriteString(req.File) - write.Flush() - - client, err := toolbox.NewFail2Ban() - if err != nil { - return err - } - if err := client.Operate("restart"); err != nil { - return err - } - return nil -} - -func (u *Fail2BanService) OperateSSHD(req dto.Fail2BanSet) error { - if req.Operate == "ignore" { - if err := u.UpdateConf(dto.Fail2BanUpdate{Key: "ignoreip", Value: strings.Join(req.IPs, ",")}); err != nil { - return err - } - return nil - } - client, err := toolbox.NewFail2Ban() - if err != nil { - return err - } - if err := client.ReBanIPs(req.IPs); err != nil { - return err - } - return nil -} - -func loadFailValue(line string, baseInfo *dto.Fail2BanBaseInfo) { - if strings.HasPrefix(line, "port") { - itemValue := strings.ReplaceAll(line, "port", "") - itemValue = strings.ReplaceAll(itemValue, "=", "") - baseInfo.Port, _ = strconv.Atoi(strings.TrimSpace(itemValue)) - } - if strings.HasPrefix(line, "maxretry") { - itemValue := strings.ReplaceAll(line, "maxretry", "") - itemValue = strings.ReplaceAll(itemValue, "=", "") - baseInfo.MaxRetry, _ = strconv.Atoi(strings.TrimSpace(itemValue)) - } - if strings.HasPrefix(line, "findtime") { - itemValue := strings.ReplaceAll(line, "findtime", "") - itemValue = strings.ReplaceAll(itemValue, "=", "") - baseInfo.FindTime = strings.TrimSpace(itemValue) - } - if strings.HasPrefix(line, "bantime") { - itemValue := strings.ReplaceAll(line, "bantime", "") - itemValue = strings.ReplaceAll(itemValue, "=", "") - baseInfo.BanTime = strings.TrimSpace(itemValue) - } - if strings.HasPrefix(line, "banaction") { - itemValue := strings.ReplaceAll(line, "banaction", "") - itemValue = strings.ReplaceAll(itemValue, "=", "") - baseInfo.BanAction = strings.TrimSpace(itemValue) - } - if strings.HasPrefix(line, "logpath") { - itemValue := strings.ReplaceAll(line, "logpath", "") - itemValue = strings.ReplaceAll(itemValue, "=", "") - baseInfo.LogPath = strings.TrimSpace(itemValue) - } -} diff --git a/backend/app/service/favorite.go b/backend/app/service/favorite.go deleted file mode 100644 index 3d2ae3a90..000000000 --- a/backend/app/service/favorite.go +++ /dev/null @@ -1,83 +0,0 @@ -package service - -import ( - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/app/dto/response" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/spf13/afero" -) - -type FavoriteService struct { -} - -type IFavoriteService interface { - Page(req dto.PageInfo) (int64, []response.FavoriteDTO, error) - Create(req request.FavoriteCreate) (*model.Favorite, error) - Delete(id uint) error -} - -func NewIFavoriteService() IFavoriteService { - return &FavoriteService{} -} - -func (f *FavoriteService) Page(req dto.PageInfo) (int64, []response.FavoriteDTO, error) { - total, favorites, err := favoriteRepo.Page(req.Page, req.PageSize) - if err != nil { - return 0, nil, err - } - var dtoFavorites []response.FavoriteDTO - for _, favorite := range favorites { - dtoFavorites = append(dtoFavorites, response.FavoriteDTO{ - Favorite: favorite, - }) - } - return total, dtoFavorites, nil -} - -func (f *FavoriteService) Create(req request.FavoriteCreate) (*model.Favorite, error) { - exist, _ := favoriteRepo.GetFirst(favoriteRepo.WithByPath(req.Path)) - if exist.ID > 0 { - return nil, buserr.New(constant.ErrFavoriteExist) - } - op := files.NewFileOp() - if !op.Stat(req.Path) { - return nil, buserr.New(constant.ErrLinkPathNotFound) - } - openFile, err := op.OpenFile(req.Path) - if err != nil { - return nil, err - } - fileInfo, err := openFile.Stat() - if err != nil { - return nil, err - } - favorite := &model.Favorite{ - Name: fileInfo.Name(), - IsDir: fileInfo.IsDir(), - Path: req.Path, - } - if fileInfo.Size() <= 10*1024*1024 { - afs := &afero.Afero{Fs: op.Fs} - cByte, err := afs.ReadFile(req.Path) - if err == nil { - if len(cByte) > 0 && !files.DetectBinary(cByte) { - favorite.IsTxt = true - } - } - } - if err := favoriteRepo.Create(favorite); err != nil { - return nil, err - } - return favorite, nil -} - -func (f *FavoriteService) Delete(id uint) error { - if err := favoriteRepo.Delete(commonRepo.WithByID(id)); err != nil { - return err - } - return nil -} diff --git a/backend/app/service/file.go b/backend/app/service/file.go deleted file mode 100644 index 76dced4d8..000000000 --- a/backend/app/service/file.go +++ /dev/null @@ -1,484 +0,0 @@ -package service - -import ( - "fmt" - "io" - "io/fs" - "os" - "os/exec" - "path" - "path/filepath" - "strings" - "time" - "unicode/utf8" - - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/app/dto/response" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "golang.org/x/net/html/charset" - "golang.org/x/sys/unix" - "golang.org/x/text/encoding/simplifiedchinese" - "golang.org/x/text/transform" - - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/pkg/errors" -) - -type FileService struct { -} - -type IFileService interface { - GetFileList(op request.FileOption) (response.FileInfo, error) - SearchUploadWithPage(req request.SearchUploadWithPage) (int64, interface{}, error) - GetFileTree(op request.FileOption) ([]response.FileTree, error) - Create(op request.FileCreate) error - Delete(op request.FileDelete) error - BatchDelete(op request.FileBatchDelete) error - Compress(c request.FileCompress) error - DeCompress(c request.FileDeCompress) error - GetContent(op request.FileContentReq) (response.FileInfo, error) - SaveContent(edit request.FileEdit) error - FileDownload(d request.FileDownload) (string, error) - DirSize(req request.DirSizeReq) (response.DirSizeRes, error) - ChangeName(req request.FileRename) error - Wget(w request.FileWget) (string, error) - MvFile(m request.FileMove) error - ChangeOwner(req request.FileRoleUpdate) error - ChangeMode(op request.FileCreate) error - BatchChangeModeAndOwner(op request.FileRoleReq) error - ReadLogByLine(req request.FileReadByLineReq) (*response.FileLineContent, error) -} - -var filteredPaths = []string{ - "/.1panel_clash", -} - -func NewIFileService() IFileService { - return &FileService{} -} - -func (f *FileService) GetFileList(op request.FileOption) (response.FileInfo, error) { - var fileInfo response.FileInfo - if _, err := os.Stat(op.Path); err != nil && os.IsNotExist(err) { - return fileInfo, nil - } - info, err := files.NewFileInfo(op.FileOption) - if err != nil { - return fileInfo, err - } - fileInfo.FileInfo = *info - return fileInfo, nil -} - -func (f *FileService) SearchUploadWithPage(req request.SearchUploadWithPage) (int64, interface{}, error) { - var ( - files []response.UploadInfo - backData []response.UploadInfo - ) - _ = filepath.Walk(req.Path, func(path string, info os.FileInfo, err error) error { - if err != nil { - return nil - } - if !info.IsDir() { - files = append(files, response.UploadInfo{ - CreatedAt: info.ModTime().Format(constant.DateTimeLayout), - Size: int(info.Size()), - Name: info.Name(), - }) - } - return nil - }) - total, start, end := len(files), (req.Page-1)*req.PageSize, req.Page*req.PageSize - if start > total { - backData = make([]response.UploadInfo, 0) - } else { - if end >= total { - end = total - } - backData = files[start:end] - } - return int64(total), backData, nil -} - -func (f *FileService) GetFileTree(op request.FileOption) ([]response.FileTree, error) { - var treeArray []response.FileTree - if _, err := os.Stat(op.Path); err != nil && os.IsNotExist(err) { - return treeArray, nil - } - info, err := files.NewFileInfo(op.FileOption) - if err != nil { - return nil, err - } - node := response.FileTree{ - ID: common.GetUuid(), - Name: info.Name, - Path: info.Path, - IsDir: info.IsDir, - Extension: info.Extension, - } - err = f.buildFileTree(&node, info.Items, op, 2) - if err != nil { - return nil, err - } - return append(treeArray, node), nil -} - -func shouldFilterPath(path string) bool { - cleanedPath := filepath.Clean(path) - for _, filteredPath := range filteredPaths { - cleanedFilteredPath := filepath.Clean(filteredPath) - if cleanedFilteredPath == cleanedPath || strings.HasPrefix(cleanedPath, cleanedFilteredPath+"/") { - return true - } - } - return false -} - -// 递归构建文件树(只取当前目录以及当前目录下的第一层子节点) -func (f *FileService) buildFileTree(node *response.FileTree, items []*files.FileInfo, op request.FileOption, level int) error { - for _, v := range items { - if shouldFilterPath(v.Path) { - global.LOG.Infof("File Tree: Skipping %s due to filter\n", v.Path) - continue - } - childNode := response.FileTree{ - ID: common.GetUuid(), - Name: v.Name, - Path: v.Path, - IsDir: v.IsDir, - Extension: v.Extension, - } - if level > 1 && v.IsDir { - if err := f.buildChildNode(&childNode, v, op, level); err != nil { - return err - } - } - - node.Children = append(node.Children, childNode) - } - return nil -} - -func (f *FileService) buildChildNode(childNode *response.FileTree, fileInfo *files.FileInfo, op request.FileOption, level int) error { - op.Path = fileInfo.Path - subInfo, err := files.NewFileInfo(op.FileOption) - if err != nil { - if os.IsPermission(err) || errors.Is(err, unix.EACCES) { - global.LOG.Infof("File Tree: Skipping %s due to permission denied\n", fileInfo.Path) - return nil - } - global.LOG.Errorf("File Tree: Skipping %s due to error: %s\n", fileInfo.Path, err.Error()) - return nil - } - - return f.buildFileTree(childNode, subInfo.Items, op, level-1) -} - -func (f *FileService) Create(op request.FileCreate) error { - if files.IsInvalidChar(op.Path) { - return buserr.New("ErrInvalidChar") - } - fo := files.NewFileOp() - if fo.Stat(op.Path) { - return buserr.New(constant.ErrFileIsExist) - } - mode := op.Mode - if mode == 0 { - fileInfo, err := os.Stat(filepath.Dir(op.Path)) - if err == nil { - mode = int64(fileInfo.Mode().Perm()) - } else { - mode = 0755 - } - } - if op.IsDir { - return fo.CreateDirWithMode(op.Path, fs.FileMode(mode)) - } - if op.IsLink { - if !fo.Stat(op.LinkPath) { - return buserr.New(constant.ErrLinkPathNotFound) - } - return fo.LinkFile(op.LinkPath, op.Path, op.IsSymlink) - } - return fo.CreateFileWithMode(op.Path, fs.FileMode(mode)) -} - -func (f *FileService) Delete(op request.FileDelete) error { - fo := files.NewFileOp() - recycleBinStatus, _ := settingRepo.Get(settingRepo.WithByKey("FileRecycleBin")) - if recycleBinStatus.Value == "disable" { - op.ForceDelete = true - } - if op.ForceDelete { - if op.IsDir { - return fo.DeleteDir(op.Path) - } else { - return fo.DeleteFile(op.Path) - } - } - if err := NewIRecycleBinService().Create(request.RecycleBinCreate{SourcePath: op.Path}); err != nil { - return err - } - return favoriteRepo.Delete(favoriteRepo.WithByPath(op.Path)) -} - -func (f *FileService) BatchDelete(op request.FileBatchDelete) error { - fo := files.NewFileOp() - if op.IsDir { - for _, file := range op.Paths { - if err := fo.DeleteDir(file); err != nil { - return err - } - } - } else { - for _, file := range op.Paths { - if err := fo.DeleteFile(file); err != nil { - return err - } - } - } - return nil -} - -func (f *FileService) ChangeMode(op request.FileCreate) error { - fo := files.NewFileOp() - return fo.ChmodR(op.Path, op.Mode, op.Sub) -} - -func (f *FileService) BatchChangeModeAndOwner(op request.FileRoleReq) error { - fo := files.NewFileOp() - for _, path := range op.Paths { - if !fo.Stat(path) { - return buserr.New(constant.ErrPathNotFound) - } - if err := fo.ChownR(path, op.User, op.Group, op.Sub); err != nil { - return err - } - if err := fo.ChmodR(path, op.Mode, op.Sub); err != nil { - return err - } - } - return nil - -} - -func (f *FileService) ChangeOwner(req request.FileRoleUpdate) error { - fo := files.NewFileOp() - return fo.ChownR(req.Path, req.User, req.Group, req.Sub) -} - -func (f *FileService) Compress(c request.FileCompress) error { - fo := files.NewFileOp() - if !c.Replace && fo.Stat(filepath.Join(c.Dst, c.Name)) { - return buserr.New(constant.ErrFileIsExist) - } - return fo.Compress(c.Files, c.Dst, c.Name, files.CompressType(c.Type), c.Secret) -} - -func (f *FileService) DeCompress(c request.FileDeCompress) error { - fo := files.NewFileOp() - if c.Type == "tar" && len(c.Secret) != 0 { - c.Type = "tar.gz" - } - return fo.Decompress(c.Path, c.Dst, files.CompressType(c.Type), c.Secret) -} - -func (f *FileService) GetContent(op request.FileContentReq) (response.FileInfo, error) { - info, err := files.NewFileInfo(files.FileOption{ - Path: op.Path, - Expand: true, - IsDetail: op.IsDetail, - }) - if err != nil { - return response.FileInfo{}, err - } - - content := []byte(info.Content) - if len(content) > 1024 { - content = content[:1024] - } - if !utf8.Valid(content) { - _, decodeName, _ := charset.DetermineEncoding(content, "") - if decodeName == "windows-1252" { - reader := strings.NewReader(info.Content) - item := transform.NewReader(reader, simplifiedchinese.GBK.NewDecoder()) - contents, err := io.ReadAll(item) - if err != nil { - return response.FileInfo{}, err - } - info.Content = string(contents) - } - } - return response.FileInfo{FileInfo: *info}, nil -} - -func (f *FileService) SaveContent(edit request.FileEdit) error { - info, err := files.NewFileInfo(files.FileOption{ - Path: edit.Path, - Expand: false, - }) - if err != nil { - return err - } - - fo := files.NewFileOp() - return fo.WriteFile(edit.Path, strings.NewReader(edit.Content), info.FileMode) -} - -func (f *FileService) ChangeName(req request.FileRename) error { - if files.IsInvalidChar(req.NewName) { - return buserr.New("ErrInvalidChar") - } - fo := files.NewFileOp() - return fo.Rename(req.OldName, req.NewName) -} - -func (f *FileService) Wget(w request.FileWget) (string, error) { - fo := files.NewFileOp() - key := "file-wget-" + common.GetUuid() - return key, fo.DownloadFileWithProcess(w.Url, filepath.Join(w.Path, w.Name), key, w.IgnoreCertificate) -} - -func (f *FileService) MvFile(m request.FileMove) error { - fo := files.NewFileOp() - if !fo.Stat(m.NewPath) { - return buserr.New(constant.ErrPathNotFound) - } - for _, oldPath := range m.OldPaths { - if !fo.Stat(oldPath) { - return buserr.WithName(constant.ErrFileNotFound, oldPath) - } - if oldPath == m.NewPath || strings.Contains(m.NewPath, filepath.Clean(oldPath)+"/") { - return buserr.New(constant.ErrMovePathFailed) - } - } - if m.Type == "cut" { - return fo.Cut(m.OldPaths, m.NewPath, m.Name, m.Cover) - } - var errs []error - if m.Type == "copy" { - for _, src := range m.OldPaths { - if err := fo.CopyAndReName(src, m.NewPath, m.Name, m.Cover); err != nil { - errs = append(errs, err) - global.LOG.Errorf("copy file [%s] to [%s] failed, err: %s", src, m.NewPath, err.Error()) - } - } - } - - var errString string - for _, err := range errs { - errString += err.Error() + "\n" - } - if errString != "" { - return errors.New(errString) - } - return nil -} - -func (f *FileService) FileDownload(d request.FileDownload) (string, error) { - filePath := d.Paths[0] - if d.Compress { - tempPath := filepath.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().UnixNano())) - if err := os.MkdirAll(tempPath, os.ModePerm); err != nil { - return "", err - } - fo := files.NewFileOp() - if err := fo.Compress(d.Paths, tempPath, d.Name, files.CompressType(d.Type), ""); err != nil { - return "", err - } - filePath = filepath.Join(tempPath, d.Name) - } - return filePath, nil -} - -func (f *FileService) DirSize(req request.DirSizeReq) (response.DirSizeRes, error) { - var ( - res response.DirSizeRes - ) - if req.Path == "/proc" { - return res, nil - } - cmd := exec.Command("du", "-s", req.Path) - output, err := cmd.Output() - if err == nil { - fields := strings.Fields(string(output)) - if len(fields) == 2 { - var cmdSize int64 - _, err = fmt.Sscanf(fields[0], "%d", &cmdSize) - if err == nil { - res.Size = float64(cmdSize * 1024) - return res, nil - } - } - } - fo := files.NewFileOp() - size, err := fo.GetDirSize(req.Path) - if err != nil { - return res, err - } - res.Size = size - return res, nil -} - -func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.FileLineContent, error) { - logFilePath := "" - switch req.Type { - case constant.TypeWebsite: - website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) - if err != nil { - return nil, err - } - nginx, err := getNginxFull(&website) - if err != nil { - return nil, err - } - sitePath := path.Join(nginx.SiteDir, "sites", website.Alias) - logFilePath = path.Join(sitePath, "log", req.Name) - case constant.TypePhp: - php, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID)) - if err != nil { - return nil, err - } - logFilePath = php.GetLogPath() - case constant.TypeSSL: - ssl, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(req.ID)) - if err != nil { - return nil, err - } - logFilePath = ssl.GetLogPath() - case constant.TypeSystem: - fileName := "" - if len(req.Name) == 0 || req.Name == time.Now().Format("2006-01-02") { - fileName = "1Panel.log" - } else { - fileName = "1Panel-" + req.Name + ".log" - } - logFilePath = path.Join(global.CONF.System.DataDir, "log", fileName) - if _, err := os.Stat(logFilePath); err != nil { - fileGzPath := path.Join(global.CONF.System.DataDir, "log", fileName+".gz") - if _, err := os.Stat(fileGzPath); err != nil { - return nil, buserr.New("ErrHttpReqNotFound") - } - if err := handleGunzip(fileGzPath); err != nil { - return nil, fmt.Errorf("handle ungzip file %s failed, err: %v", fileGzPath, err) - } - } - case "image-pull", "image-push", "image-build", "compose-create": - logFilePath = path.Join(global.CONF.System.TmpDir, fmt.Sprintf("docker_logs/%s", req.Name)) - } - - lines, isEndOfFile, total, err := files.ReadFileByLine(logFilePath, req.Page, req.PageSize, req.Latest) - if err != nil { - return nil, err - } - res := &response.FileLineContent{ - Content: strings.Join(lines, "\n"), - End: isEndOfFile, - Path: logFilePath, - Total: total, - } - return res, nil -} diff --git a/backend/app/service/firewall.go b/backend/app/service/firewall.go deleted file mode 100644 index b5a45860a..000000000 --- a/backend/app/service/firewall.go +++ /dev/null @@ -1,694 +0,0 @@ -package service - -import ( - "fmt" - "os" - "sort" - "strconv" - "strings" - "sync" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/1Panel-dev/1Panel/backend/utils/firewall" - fireClient "github.com/1Panel-dev/1Panel/backend/utils/firewall/client" - "github.com/jinzhu/copier" - "github.com/pkg/errors" -) - -const confPath = "/etc/sysctl.conf" - -type FirewallService struct{} - -type IFirewallService interface { - LoadBaseInfo() (dto.FirewallBaseInfo, error) - SearchWithPage(search dto.RuleSearch) (int64, interface{}, error) - OperateFirewall(operation string) error - OperatePortRule(req dto.PortRuleOperate, reload bool) error - OperateForwardRule(req dto.ForwardRuleOperate) error - OperateAddressRule(req dto.AddrRuleOperate, reload bool) error - UpdatePortRule(req dto.PortRuleUpdate) error - UpdateAddrRule(req dto.AddrRuleUpdate) error - UpdateDescription(req dto.UpdateFirewallDescription) error - BatchOperateRule(req dto.BatchRuleOperate) error -} - -func NewIFirewallService() IFirewallService { - return &FirewallService{} -} - -func (u *FirewallService) LoadBaseInfo() (dto.FirewallBaseInfo, error) { - var baseInfo dto.FirewallBaseInfo - baseInfo.Status = "not running" - baseInfo.Version = "-" - baseInfo.Name = "-" - client, err := firewall.NewFirewallClient() - if err != nil { - return baseInfo, err - } - baseInfo.Name = client.Name() - - var wg sync.WaitGroup - wg.Add(3) - go func() { - defer wg.Done() - baseInfo.PingStatus = u.pingStatus() - }() - go func() { - defer wg.Done() - baseInfo.Status, _ = client.Status() - }() - go func() { - defer wg.Done() - baseInfo.Version, _ = client.Version() - }() - wg.Wait() - return baseInfo, nil -} - -func (u *FirewallService) SearchWithPage(req dto.RuleSearch) (int64, interface{}, error) { - var ( - datas []fireClient.FireInfo - backDatas []fireClient.FireInfo - ) - - client, err := firewall.NewFirewallClient() - if err != nil { - return 0, nil, err - } - - var rules []fireClient.FireInfo - switch req.Type { - case "port": - rules, err = client.ListPort() - case "forward": - rules, err = client.ListForward() - case "address": - rules, err = client.ListAddress() - } - if err != nil { - return 0, nil, err - } - - if len(req.Info) != 0 { - for _, addr := range rules { - if strings.Contains(addr.Address, req.Info) || - strings.Contains(addr.Port, req.Info) || - strings.Contains(addr.TargetPort, req.Info) || - strings.Contains(addr.TargetIP, req.Info) { - datas = append(datas, addr) - } - } - } else { - datas = rules - } - if req.Type == "port" { - apps := u.loadPortByApp() - for i := 0; i < len(datas); i++ { - datas[i].UsedStatus = checkPortUsed(datas[i].Port, datas[i].Protocol, apps) - } - } - - var datasFilterStatus []fireClient.FireInfo - if len(req.Status) != 0 { - for _, data := range datas { - if req.Status == "free" && len(data.UsedStatus) == 0 { - datasFilterStatus = append(datasFilterStatus, data) - } - if req.Status == "used" && len(data.UsedStatus) != 0 { - datasFilterStatus = append(datasFilterStatus, data) - } - } - } else { - datasFilterStatus = datas - } - - var datasFilterStrategy []fireClient.FireInfo - if len(req.Strategy) != 0 { - for _, data := range datasFilterStatus { - if req.Strategy == data.Strategy { - datasFilterStrategy = append(datasFilterStrategy, data) - } - } - } else { - datasFilterStrategy = datasFilterStatus - } - - total, start, end := len(datasFilterStrategy), (req.Page-1)*req.PageSize, req.Page*req.PageSize - if start > total { - backDatas = make([]fireClient.FireInfo, 0) - } else { - if end >= total { - end = total - } - backDatas = datasFilterStrategy[start:end] - } - - datasFromDB, _ := hostRepo.ListFirewallRecord() - for i := 0; i < len(backDatas); i++ { - for _, des := range datasFromDB { - if req.Type != des.Type { - continue - } - if backDatas[i].Port == des.Port && - req.Type == "port" && - backDatas[i].Protocol == des.Protocol && - backDatas[i].Strategy == des.Strategy && - backDatas[i].Address == des.Address { - backDatas[i].Description = des.Description - break - } - if req.Type == "address" && backDatas[i].Strategy == des.Strategy && backDatas[i].Address == des.Address { - backDatas[i].Description = des.Description - break - } - } - } - - go u.cleanUnUsedData(client) - - return int64(total), backDatas, nil -} - -func (u *FirewallService) OperateFirewall(operation string) error { - client, err := firewall.NewFirewallClient() - if err != nil { - return err - } - switch operation { - case "start": - if err := client.Start(); err != nil { - return err - } - if err := u.addPortsBeforeStart(client); err != nil { - _ = client.Stop() - return err - } - _, _ = cmd.Exec("systemctl restart docker") - return nil - case "stop": - if err := client.Stop(); err != nil { - return err - } - _, _ = cmd.Exec("systemctl restart docker") - return nil - case "restart": - if err := client.Restart(); err != nil { - return err - } - _, _ = cmd.Exec("systemctl restart docker") - return nil - case "disablePing": - return u.updatePingStatus("0") - case "enablePing": - return u.updatePingStatus("1") - } - return fmt.Errorf("not support such operation: %s", operation) -} - -func (u *FirewallService) OperatePortRule(req dto.PortRuleOperate, reload bool) error { - client, err := firewall.NewFirewallClient() - if err != nil { - return err - } - protos := strings.Split(req.Protocol, "/") - itemAddress := strings.Split(strings.TrimSuffix(req.Address, ","), ",") - - if client.Name() == "ufw" { - if strings.Contains(req.Port, ",") || strings.Contains(req.Port, "-") { - for _, proto := range protos { - for _, addr := range itemAddress { - if len(addr) == 0 { - addr = "Anywhere" - } - req.Address = addr - req.Port = strings.ReplaceAll(req.Port, "-", ":") - req.Protocol = proto - if err := u.operatePort(client, req); err != nil { - return err - } - req.Port = strings.ReplaceAll(req.Port, ":", "-") - if err := u.addPortRecord(req); err != nil { - return err - } - } - } - return nil - } - for _, addr := range itemAddress { - if len(addr) == 0 { - addr = "Anywhere" - } - if req.Protocol == "tcp/udp" { - req.Protocol = "" - } - req.Address = addr - if err := u.operatePort(client, req); err != nil { - return err - } - if len(req.Protocol) == 0 { - req.Protocol = "tcp/udp" - } - if err := u.addPortRecord(req); err != nil { - return err - } - } - return nil - } - - itemPorts := req.Port - for _, proto := range protos { - if strings.Contains(req.Port, "-") { - for _, addr := range itemAddress { - req.Protocol = proto - req.Address = addr - if err := u.operatePort(client, req); err != nil { - return err - } - if err := u.addPortRecord(req); err != nil { - return err - } - } - } else { - ports := strings.Split(itemPorts, ",") - for _, port := range ports { - if len(port) == 0 { - continue - } - for _, addr := range itemAddress { - req.Address = addr - req.Port = port - req.Protocol = proto - if err := u.operatePort(client, req); err != nil { - return err - } - if err := u.addPortRecord(req); err != nil { - return err - } - } - } - } - } - - if reload { - return client.Reload() - } - return nil -} - -func (u *FirewallService) OperateForwardRule(req dto.ForwardRuleOperate) error { - client, err := firewall.NewFirewallClient() - if err != nil { - return err - } - - rules, _ := client.ListForward() - for _, rule := range rules { - for _, reqRule := range req.Rules { - if reqRule.Operation == "remove" { - continue - } - if reqRule.TargetIP == "" { - reqRule.TargetIP = "127.0.0.1" - } - if reqRule.Port == rule.Port && reqRule.TargetPort == rule.TargetPort && reqRule.TargetIP == rule.TargetIP { - return constant.ErrRecordExist - } - } - } - - sort.SliceStable(req.Rules, func(i, j int) bool { - n1, _ := strconv.Atoi(req.Rules[i].Num) - n2, _ := strconv.Atoi(req.Rules[j].Num) - return n1 > n2 - }) - - for _, r := range req.Rules { - for _, p := range strings.Split(r.Protocol, "/") { - if r.TargetIP == "" { - r.TargetIP = "127.0.0.1" - } - if err = client.PortForward(fireClient.Forward{ - Num: r.Num, - Protocol: p, - Port: r.Port, - TargetIP: r.TargetIP, - TargetPort: r.TargetPort, - }, r.Operation); err != nil { - return err - } - } - } - return nil -} - -func (u *FirewallService) OperateAddressRule(req dto.AddrRuleOperate, reload bool) error { - client, err := firewall.NewFirewallClient() - if err != nil { - return err - } - - var fireInfo fireClient.FireInfo - if err := copier.Copy(&fireInfo, &req); err != nil { - return err - } - - addressList := strings.Split(req.Address, ",") - for i := 0; i < len(addressList); i++ { - if len(addressList[i]) == 0 { - continue - } - fireInfo.Address = addressList[i] - if err := client.RichRules(fireInfo, req.Operation); err != nil { - return err - } - req.Address = addressList[i] - if err := u.addAddressRecord(req); err != nil { - return err - } - } - if reload { - return client.Reload() - } - return nil -} - -func (u *FirewallService) UpdatePortRule(req dto.PortRuleUpdate) error { - client, err := firewall.NewFirewallClient() - if err != nil { - return err - } - if err := u.OperatePortRule(req.OldRule, false); err != nil { - return err - } - if err := u.OperatePortRule(req.NewRule, false); err != nil { - return err - } - return client.Reload() -} - -func (u *FirewallService) UpdateAddrRule(req dto.AddrRuleUpdate) error { - client, err := firewall.NewFirewallClient() - if err != nil { - return err - } - if err := u.OperateAddressRule(req.OldRule, false); err != nil { - return err - } - if err := u.OperateAddressRule(req.NewRule, false); err != nil { - return err - } - return client.Reload() -} - -func (u *FirewallService) UpdateDescription(req dto.UpdateFirewallDescription) error { - var firewall model.Firewall - if err := copier.Copy(&firewall, &req); err != nil { - return errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - return hostRepo.SaveFirewallRecord(&firewall) -} - -func (u *FirewallService) BatchOperateRule(req dto.BatchRuleOperate) error { - client, err := firewall.NewFirewallClient() - if err != nil { - return err - } - if req.Type == "port" { - for _, rule := range req.Rules { - _ = u.OperatePortRule(rule, false) - } - return client.Reload() - } - for _, rule := range req.Rules { - itemRule := dto.AddrRuleOperate{Operation: rule.Operation, Address: rule.Address, Strategy: rule.Strategy} - _ = u.OperateAddressRule(itemRule, false) - } - return client.Reload() -} - -func OperateFirewallPort(oldPorts, newPorts []int) error { - client, err := firewall.NewFirewallClient() - if err != nil { - return err - } - for _, port := range newPorts { - if err := client.Port(fireClient.FireInfo{Port: strconv.Itoa(port), Protocol: "tcp", Strategy: "accept"}, "add"); err != nil { - return err - } - } - for _, port := range oldPorts { - if err := client.Port(fireClient.FireInfo{Port: strconv.Itoa(port), Protocol: "tcp", Strategy: "accept"}, "remove"); err != nil { - return err - } - } - return client.Reload() -} - -func (u *FirewallService) operatePort(client firewall.FirewallClient, req dto.PortRuleOperate) error { - var fireInfo fireClient.FireInfo - if err := copier.Copy(&fireInfo, &req); err != nil { - return err - } - - if client.Name() == "ufw" { - if len(fireInfo.Address) != 0 && !strings.EqualFold(fireInfo.Address, "Anywhere") { - return client.RichRules(fireInfo, req.Operation) - } - return client.Port(fireInfo, req.Operation) - } - - if len(fireInfo.Address) != 0 || fireInfo.Strategy == "drop" { - return client.RichRules(fireInfo, req.Operation) - } - return client.Port(fireInfo, req.Operation) -} - -type portOfApp struct { - AppName string - HttpPort string - HttpsPort string -} - -func (u *FirewallService) loadPortByApp() []portOfApp { - var datas []portOfApp - apps, err := appInstallRepo.ListBy() - if err != nil { - return datas - } - for i := 0; i < len(apps); i++ { - datas = append(datas, portOfApp{ - AppName: apps[i].App.Key, - HttpPort: strconv.Itoa(apps[i].HttpPort), - HttpsPort: strconv.Itoa(apps[i].HttpsPort), - }) - } - systemPort, err := settingRepo.Get(settingRepo.WithByKey("ServerPort")) - if err != nil { - return datas - } - datas = append(datas, portOfApp{AppName: "1panel", HttpPort: systemPort.Value}) - - return datas -} - -func (u *FirewallService) cleanUnUsedData(client firewall.FirewallClient) { - list, _ := client.ListPort() - addressList, _ := client.ListAddress() - list = append(list, addressList...) - if len(list) == 0 { - return - } - records, _ := hostRepo.ListFirewallRecord() - if len(records) == 0 { - return - } - for _, item := range list { - for i := 0; i < len(records); i++ { - if records[i].Port == item.Port && records[i].Protocol == item.Protocol && records[i].Strategy == item.Strategy && records[i].Address == item.Address { - records = append(records[:i], records[i+1:]...) - } - } - } - - for _, record := range records { - _ = hostRepo.DeleteFirewallRecordByID(record.ID) - } -} -func (u *FirewallService) pingStatus() string { - if _, err := os.Stat("/etc/sysctl.conf"); err != nil { - return constant.StatusNone - } - sudo := cmd.SudoHandleCmd() - command := fmt.Sprintf("%s cat /etc/sysctl.conf | grep net/ipv4/icmp_echo_ignore_all= ", sudo) - stdout, _ := cmd.Exec(command) - if stdout == "net/ipv4/icmp_echo_ignore_all=1\n" { - return constant.StatusEnable - } - return constant.StatusDisable -} - -func (u *FirewallService) updatePingStatus(enable string) error { - lineBytes, err := os.ReadFile(confPath) - if err != nil { - return err - } - files := strings.Split(string(lineBytes), "\n") - var newFiles []string - hasLine := false - for _, line := range files { - if strings.Contains(line, "net/ipv4/icmp_echo_ignore_all") || strings.HasPrefix(line, "net/ipv4/icmp_echo_ignore_all") { - newFiles = append(newFiles, "net/ipv4/icmp_echo_ignore_all="+enable) - hasLine = true - } else { - newFiles = append(newFiles, line) - } - } - if !hasLine { - newFiles = append(newFiles, "net/ipv4/icmp_echo_ignore_all="+enable) - } - file, err := os.OpenFile(confPath, os.O_WRONLY|os.O_TRUNC, 0666) - if err != nil { - return err - } - defer file.Close() - _, err = file.WriteString(strings.Join(newFiles, "\n")) - if err != nil { - return err - } - - sudo := cmd.SudoHandleCmd() - command := fmt.Sprintf("%s sysctl -p", sudo) - stdout, err := cmd.Exec(command) - if err != nil { - return fmt.Errorf("update ping status failed, err: %v", stdout) - } - - return nil -} - -func (u *FirewallService) addPortsBeforeStart(client firewall.FirewallClient) error { - serverPort, err := settingRepo.Get(settingRepo.WithByKey("ServerPort")) - if err != nil { - return err - } - if err := client.Port(fireClient.FireInfo{Port: serverPort.Value, Protocol: "tcp", Strategy: "accept"}, "add"); err != nil { - return err - } - if err := client.Port(fireClient.FireInfo{Port: "22", Protocol: "tcp", Strategy: "accept"}, "add"); err != nil { - return err - } - if err := client.Port(fireClient.FireInfo{Port: "80", Protocol: "tcp", Strategy: "accept"}, "add"); err != nil { - return err - } - if err := client.Port(fireClient.FireInfo{Port: "443", Protocol: "tcp", Strategy: "accept"}, "add"); err != nil { - return err - } - apps := u.loadPortByApp() - for _, app := range apps { - if len(app.HttpPort) != 0 && app.HttpPort != "0" { - if err := client.Port(fireClient.FireInfo{Port: app.HttpPort, Protocol: "tcp", Strategy: "accept"}, "add"); err != nil { - return err - } - } - } - - return client.Reload() -} - -func (u *FirewallService) addPortRecord(req dto.PortRuleOperate) error { - if req.Operation == "remove" { - return hostRepo.DeleteFirewallRecord("port", req.Port, req.Protocol, req.Address, req.Strategy) - } - - if err := hostRepo.SaveFirewallRecord(&model.Firewall{ - Type: "port", - Port: req.Port, - Protocol: req.Protocol, - Address: req.Address, - Strategy: req.Strategy, - Description: req.Description, - }); err != nil { - return fmt.Errorf("add record %s/%s failed (strategy: %s, address: %s), err: %v", req.Port, req.Protocol, req.Strategy, req.Address, err) - } - - return nil -} - -func (u *FirewallService) addAddressRecord(req dto.AddrRuleOperate) error { - if req.Operation == "remove" { - return hostRepo.DeleteFirewallRecord("address", "", "", req.Address, req.Strategy) - } - if err := hostRepo.SaveFirewallRecord(&model.Firewall{ - Type: "address", - Address: req.Address, - Strategy: req.Strategy, - Description: req.Description, - }); err != nil { - return fmt.Errorf("add record failed (strategy: %s, address: %s), err: %v", req.Strategy, req.Address, err) - } - return nil -} - -func checkPortUsed(ports, proto string, apps []portOfApp) string { - var portList []int - if strings.Contains(ports, "-") || strings.Contains(ports, ",") { - if strings.Contains(ports, "-") { - port1, err := strconv.Atoi(strings.Split(ports, "-")[0]) - if err != nil { - global.LOG.Errorf(" convert string %s to int failed, err: %v", strings.Split(ports, "-")[0], err) - return "" - } - port2, err := strconv.Atoi(strings.Split(ports, "-")[1]) - if err != nil { - global.LOG.Errorf(" convert string %s to int failed, err: %v", strings.Split(ports, "-")[1], err) - return "" - } - for i := port1; i <= port2; i++ { - portList = append(portList, i) - } - } else { - portLists := strings.Split(ports, ",") - for _, item := range portLists { - portItem, _ := strconv.Atoi(item) - portList = append(portList, portItem) - } - } - - var usedPorts []string - for _, port := range portList { - portItem := fmt.Sprintf("%v", port) - isUsedByApp := false - for _, app := range apps { - if app.HttpPort == portItem || app.HttpsPort == portItem { - isUsedByApp = true - usedPorts = append(usedPorts, fmt.Sprintf("%s (%s)", portItem, app.AppName)) - break - } - } - if !isUsedByApp && common.ScanPortWithProto(port, proto) { - usedPorts = append(usedPorts, fmt.Sprintf("%v", port)) - } - } - return strings.Join(usedPorts, ",") - } - - for _, app := range apps { - if app.HttpPort == ports || app.HttpsPort == ports { - return fmt.Sprintf("(%s)", app.AppName) - } - } - port, err := strconv.Atoi(ports) - if err != nil { - global.LOG.Errorf(" convert string %v to int failed, err: %v", port, err) - return "" - } - if common.ScanPortWithProto(port, proto) { - return "inUsed" - } - return "" -} diff --git a/backend/app/service/ftp.go b/backend/app/service/ftp.go deleted file mode 100644 index bd9048afd..000000000 --- a/backend/app/service/ftp.go +++ /dev/null @@ -1,245 +0,0 @@ -package service - -import ( - "os" - "sort" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/encrypt" - "github.com/1Panel-dev/1Panel/backend/utils/toolbox" - "github.com/jinzhu/copier" - "github.com/pkg/errors" -) - -type FtpService struct{} - -type IFtpService interface { - LoadBaseInfo() (dto.FtpBaseInfo, error) - SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error) - Operate(operation string) error - Create(req dto.FtpCreate) (uint, error) - Delete(req dto.BatchDeleteReq) error - Update(req dto.FtpUpdate) error - Sync() error - LoadLog(req dto.FtpLogSearch) (int64, interface{}, error) -} - -func NewIFtpService() IFtpService { - return &FtpService{} -} - -func (f *FtpService) LoadBaseInfo() (dto.FtpBaseInfo, error) { - var baseInfo dto.FtpBaseInfo - client, err := toolbox.NewFtpClient() - if err != nil { - return baseInfo, err - } - baseInfo.IsActive, baseInfo.IsExist = client.Status() - return baseInfo, nil -} - -func (f *FtpService) LoadLog(req dto.FtpLogSearch) (int64, interface{}, error) { - client, err := toolbox.NewFtpClient() - if err != nil { - return 0, nil, err - } - logItem, err := client.LoadLogs(req.User, req.Operation) - if err != nil { - return 0, nil, err - } - sort.Slice(logItem, func(i, j int) bool { - return logItem[i].Time > logItem[j].Time - }) - var logs []toolbox.FtpLog - total, start, end := len(logItem), (req.Page-1)*req.PageSize, req.Page*req.PageSize - if start > total { - logs = make([]toolbox.FtpLog, 0) - } else { - if end >= total { - end = total - } - logs = logItem[start:end] - } - return int64(total), logs, nil -} - -func (u *FtpService) Operate(operation string) error { - client, err := toolbox.NewFtpClient() - if err != nil { - return err - } - return client.Operate(operation) -} - -func (f *FtpService) SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error) { - total, lists, err := ftpRepo.Page(req.Page, req.PageSize, ftpRepo.WithByUser(req.Info), commonRepo.WithOrderBy("created_at desc")) - if err != nil { - return 0, nil, err - } - var users []dto.FtpInfo - for _, user := range lists { - var item dto.FtpInfo - if err := copier.Copy(&item, &user); err != nil { - return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - item.Password, _ = encrypt.StringDecrypt(item.Password) - users = append(users, item) - } - return total, users, err -} - -func (f *FtpService) Sync() error { - client, err := toolbox.NewFtpClient() - if err != nil { - return err - } - lists, err := client.LoadList() - if err != nil { - return nil - } - listsInDB, err := ftpRepo.GetList() - if err != nil { - return err - } - sameData := make(map[string]struct{}) - for _, item := range lists { - for _, itemInDB := range listsInDB { - if item.User == itemInDB.User { - sameData[item.User] = struct{}{} - if item.Path != itemInDB.Path || item.Status != itemInDB.Status { - _ = ftpRepo.Update(itemInDB.ID, map[string]interface{}{"path": item.Path, "status": item.Status}) - } - break - } - } - } - for _, item := range lists { - if _, ok := sameData[item.User]; !ok { - _ = ftpRepo.Create(&model.Ftp{User: item.User, Path: item.Path, Status: item.Status}) - } - } - for _, item := range listsInDB { - if _, ok := sameData[item.User]; !ok { - _ = ftpRepo.Update(item.ID, map[string]interface{}{"status": "deleted"}) - } - } - return nil -} - -func (f *FtpService) Create(req dto.FtpCreate) (uint, error) { - if _, err := os.Stat(req.Path); err != nil { - if os.IsNotExist(err) { - if err := os.MkdirAll(req.Path, os.ModePerm); err != nil { - return 0, err - } - } else { - return 0, err - } - } - pass, err := encrypt.StringEncrypt(req.Password) - if err != nil { - return 0, err - } - userInDB, _ := ftpRepo.Get(hostRepo.WithByUser(req.User)) - if userInDB.ID != 0 { - return 0, constant.ErrRecordExist - } - client, err := toolbox.NewFtpClient() - if err != nil { - return 0, err - } - if err := client.UserAdd(req.User, req.Password, req.Path); err != nil { - return 0, err - } - var ftp model.Ftp - if err := copier.Copy(&ftp, &req); err != nil { - return 0, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - ftp.Status = constant.StatusEnable - ftp.Password = pass - if err := ftpRepo.Create(&ftp); err != nil { - return 0, err - } - return ftp.ID, nil -} - -func (f *FtpService) Delete(req dto.BatchDeleteReq) error { - client, err := toolbox.NewFtpClient() - if err != nil { - return err - } - for _, id := range req.Ids { - ftpItem, err := ftpRepo.Get(commonRepo.WithByID(id)) - if err != nil { - return err - } - _ = client.UserDel(ftpItem.User) - _ = ftpRepo.Delete(commonRepo.WithByID(id)) - } - return nil -} - -func (f *FtpService) Update(req dto.FtpUpdate) error { - if _, err := os.Stat(req.Path); err != nil { - if os.IsNotExist(err) { - if err := os.MkdirAll(req.Path, os.ModePerm); err != nil { - return err - } - } else { - return err - } - } - - pass, err := encrypt.StringEncrypt(req.Password) - if err != nil { - return err - } - ftpItem, _ := ftpRepo.Get(commonRepo.WithByID(req.ID)) - if ftpItem.ID == 0 { - return constant.ErrRecordNotFound - } - passItem, err := encrypt.StringDecrypt(ftpItem.Password) - if err != nil { - return err - } - - client, err := toolbox.NewFtpClient() - if err != nil { - return err - } - needReload := false - updates := make(map[string]interface{}) - if req.Password != passItem { - if err := client.SetPasswd(ftpItem.User, req.Password); err != nil { - return err - } - updates["password"] = pass - needReload = true - } - if req.Status != ftpItem.Status { - if err := client.SetStatus(ftpItem.User, req.Status); err != nil { - return err - } - updates["status"] = req.Status - needReload = true - } - if req.Path != ftpItem.Path { - if err := client.SetPath(ftpItem.User, req.Path); err != nil { - return err - } - updates["path"] = req.Path - needReload = true - } - if req.Description != ftpItem.Description { - updates["description"] = req.Description - } - if needReload { - _ = client.Reload() - } - if len(updates) != 0 { - return ftpRepo.Update(ftpItem.ID, updates) - } - return nil -} diff --git a/backend/app/service/group.go b/backend/app/service/group.go deleted file mode 100644 index 33823bdb3..000000000 --- a/backend/app/service/group.go +++ /dev/null @@ -1,90 +0,0 @@ -package service - -import ( - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/jinzhu/copier" - "github.com/pkg/errors" -) - -type GroupService struct{} - -type IGroupService interface { - List(req dto.GroupSearch) ([]dto.GroupInfo, error) - Create(req dto.GroupCreate) error - Update(req dto.GroupUpdate) error - Delete(id uint) error -} - -func NewIGroupService() IGroupService { - return &GroupService{} -} - -func (u *GroupService) List(req dto.GroupSearch) ([]dto.GroupInfo, error) { - groups, err := groupRepo.GetList(commonRepo.WithByType(req.Type), commonRepo.WithOrderBy("is_default desc"), commonRepo.WithOrderBy("created_at desc")) - if err != nil { - return nil, constant.ErrRecordNotFound - } - var dtoUsers []dto.GroupInfo - for _, group := range groups { - var item dto.GroupInfo - if err := copier.Copy(&item, &group); err != nil { - return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - dtoUsers = append(dtoUsers, item) - } - return dtoUsers, err -} - -func (u *GroupService) Create(req dto.GroupCreate) error { - group, _ := groupRepo.Get(commonRepo.WithByName(req.Name), commonRepo.WithByType(req.Type)) - if group.ID != 0 { - return constant.ErrRecordExist - } - if err := copier.Copy(&group, &req); err != nil { - return errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - if err := groupRepo.Create(&group); err != nil { - return err - } - return nil -} - -func (u *GroupService) Delete(id uint) error { - group, _ := groupRepo.Get(commonRepo.WithByID(id)) - if group.ID == 0 { - return constant.ErrRecordNotFound - } - switch group.Type { - case "website": - websites, _ := websiteRepo.GetBy(websiteRepo.WithGroupID(id)) - if len(websites) > 0 { - return buserr.New(constant.ErrGroupIsUsed) - } - case "command": - commands, _ := commandRepo.GetList(commonRepo.WithByGroupID(id)) - if len(commands) > 0 { - return buserr.New(constant.ErrGroupIsUsed) - } - case "host": - hosts, _ := hostRepo.GetList(commonRepo.WithByGroupID(id)) - if len(hosts) > 0 { - return buserr.New(constant.ErrGroupIsUsed) - } - } - return groupRepo.Delete(commonRepo.WithByID(id)) -} - -func (u *GroupService) Update(req dto.GroupUpdate) error { - if req.IsDefault { - if err := groupRepo.CancelDefault(req.Type); err != nil { - return err - } - } - upMap := make(map[string]interface{}) - upMap["name"] = req.Name - upMap["is_default"] = req.IsDefault - - return groupRepo.Update(req.ID, upMap) -} diff --git a/backend/app/service/helper.go b/backend/app/service/helper.go deleted file mode 100644 index d498ec1b6..000000000 --- a/backend/app/service/helper.go +++ /dev/null @@ -1,14 +0,0 @@ -package service - -import ( - "context" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "gorm.io/gorm" -) - -func getTxAndContext() (tx *gorm.DB, ctx context.Context) { - tx = global.DB.Begin() - ctx = context.WithValue(context.Background(), constant.DB, tx) - return -} diff --git a/backend/app/service/host.go b/backend/app/service/host.go deleted file mode 100644 index c294a4b9e..000000000 --- a/backend/app/service/host.go +++ /dev/null @@ -1,325 +0,0 @@ -package service - -import ( - "encoding/base64" - "fmt" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/encrypt" - "github.com/1Panel-dev/1Panel/backend/utils/ssh" - "github.com/jinzhu/copier" - "github.com/pkg/errors" -) - -type HostService struct{} - -type IHostService interface { - TestLocalConn(id uint) bool - TestByInfo(req dto.HostConnTest) bool - GetHostInfo(id uint) (*model.Host, error) - SearchForTree(search dto.SearchForTree) ([]dto.HostTree, error) - SearchWithPage(search dto.SearchHostWithPage) (int64, interface{}, error) - Create(hostDto dto.HostOperate) (*dto.HostInfo, error) - Update(id uint, upMap map[string]interface{}) error - Delete(id []uint) error - - EncryptHost(itemVal string) (string, error) -} - -func NewIHostService() IHostService { - return &HostService{} -} - -func (u *HostService) TestByInfo(req dto.HostConnTest) bool { - if req.AuthMode == "password" && len(req.Password) != 0 { - password, err := base64.StdEncoding.DecodeString(req.Password) - if err != nil { - return false - } - req.Password = string(password) - } - if req.AuthMode == "key" && len(req.PrivateKey) != 0 { - privateKey, err := base64.StdEncoding.DecodeString(req.PrivateKey) - if err != nil { - return false - } - req.PrivateKey = string(privateKey) - } - if len(req.Password) == 0 && len(req.PrivateKey) == 0 { - host, err := hostRepo.Get(hostRepo.WithByAddr(req.Addr)) - if err != nil { - return false - } - req.Password = host.Password - req.AuthMode = host.AuthMode - req.PrivateKey = host.PrivateKey - req.PassPhrase = host.PassPhrase - } - - var connInfo ssh.ConnInfo - _ = copier.Copy(&connInfo, &req) - connInfo.PrivateKey = []byte(req.PrivateKey) - if len(req.PassPhrase) != 0 { - connInfo.PassPhrase = []byte(req.PassPhrase) - } - client, err := connInfo.NewClient() - if err != nil { - return false - } - defer client.Close() - return true -} - -func (u *HostService) TestLocalConn(id uint) bool { - var ( - host model.Host - err error - ) - if id == 0 { - host, err = hostRepo.Get(hostRepo.WithByAddr("127.0.0.1")) - if err != nil { - return false - } - } else { - host, err = hostRepo.Get(commonRepo.WithByID(id)) - if err != nil { - return false - } - } - var connInfo ssh.ConnInfo - if err := copier.Copy(&connInfo, &host); err != nil { - return false - } - if len(host.Password) != 0 { - host.Password, err = encrypt.StringDecrypt(host.Password) - if err != nil { - return false - } - connInfo.Password = host.Password - } - if len(host.PrivateKey) != 0 { - host.PrivateKey, err = encrypt.StringDecrypt(host.PrivateKey) - if err != nil { - return false - } - connInfo.PrivateKey = []byte(host.PrivateKey) - } - if len(host.PassPhrase) != 0 { - host.PassPhrase, err = encrypt.StringDecrypt(host.PassPhrase) - if err != nil { - return false - } - connInfo.PassPhrase = []byte(host.PassPhrase) - } - client, err := connInfo.NewClient() - if err != nil { - return false - } - defer client.Close() - - return true -} - -func (u *HostService) GetHostInfo(id uint) (*model.Host, error) { - host, err := hostRepo.Get(commonRepo.WithByID(id)) - if err != nil { - return nil, constant.ErrRecordNotFound - } - if len(host.Password) != 0 { - host.Password, err = encrypt.StringDecrypt(host.Password) - if err != nil { - return nil, err - } - } - if len(host.PrivateKey) != 0 { - host.PrivateKey, err = encrypt.StringDecrypt(host.PrivateKey) - if err != nil { - return nil, err - } - } - - if len(host.PassPhrase) != 0 { - host.PassPhrase, err = encrypt.StringDecrypt(host.PassPhrase) - if err != nil { - return nil, err - } - } - return &host, err -} - -func (u *HostService) SearchWithPage(search dto.SearchHostWithPage) (int64, interface{}, error) { - total, hosts, err := hostRepo.Page(search.Page, search.PageSize, hostRepo.WithByInfo(search.Info), commonRepo.WithByGroupID(search.GroupID)) - if err != nil { - return 0, nil, err - } - var dtoHosts []dto.HostInfo - for _, host := range hosts { - var item dto.HostInfo - if err := copier.Copy(&item, &host); err != nil { - return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - group, _ := groupRepo.Get(commonRepo.WithByID(host.GroupID)) - item.GroupBelong = group.Name - if !item.RememberPassword { - item.Password = "" - item.PrivateKey = "" - item.PassPhrase = "" - } else { - if len(host.Password) != 0 { - item.Password, err = encrypt.StringDecrypt(host.Password) - if err != nil { - return 0, nil, err - } - } - if len(host.PrivateKey) != 0 { - item.PrivateKey, err = encrypt.StringDecrypt(host.PrivateKey) - if err != nil { - return 0, nil, err - } - } - if len(host.PassPhrase) != 0 { - item.PassPhrase, err = encrypt.StringDecrypt(host.PassPhrase) - if err != nil { - return 0, nil, err - } - } - } - dtoHosts = append(dtoHosts, item) - } - return total, dtoHosts, err -} - -func (u *HostService) SearchForTree(search dto.SearchForTree) ([]dto.HostTree, error) { - hosts, err := hostRepo.GetList(hostRepo.WithByInfo(search.Info)) - if err != nil { - return nil, err - } - groups, err := groupRepo.GetList(commonRepo.WithByType("host")) - if err != nil { - return nil, err - } - var datas []dto.HostTree - for _, group := range groups { - var data dto.HostTree - data.ID = group.ID + 10000 - data.Label = group.Name - for _, host := range hosts { - label := fmt.Sprintf("%s@%s:%d", host.User, host.Addr, host.Port) - if len(host.Name) != 0 { - label = fmt.Sprintf("%s - %s@%s:%d", host.Name, host.User, host.Addr, host.Port) - } - if host.GroupID == group.ID { - data.Children = append(data.Children, dto.TreeChild{ID: host.ID, Label: label}) - } - } - if len(data.Children) != 0 { - datas = append(datas, data) - } - } - return datas, err -} - -func (u *HostService) Create(req dto.HostOperate) (*dto.HostInfo, error) { - var err error - if len(req.Password) != 0 && req.AuthMode == "password" { - req.Password, err = u.EncryptHost(req.Password) - if err != nil { - return nil, err - } - req.PrivateKey = "" - req.PassPhrase = "" - } - if len(req.PrivateKey) != 0 && req.AuthMode == "key" { - req.PrivateKey, err = u.EncryptHost(req.PrivateKey) - if err != nil { - return nil, err - } - if len(req.PassPhrase) != 0 { - req.PassPhrase, err = encrypt.StringEncrypt(req.PassPhrase) - if err != nil { - return nil, err - } - } - req.Password = "" - } - var host model.Host - if err := copier.Copy(&host, &req); err != nil { - return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - if req.GroupID == 0 { - group, err := groupRepo.Get(groupRepo.WithByHostDefault()) - if err != nil { - return nil, errors.New("get default group failed") - } - host.GroupID = group.ID - req.GroupID = group.ID - } - var sameHostID uint - if req.Addr == "127.0.0.1" { - hostSame, _ := hostRepo.Get(hostRepo.WithByAddr(req.Addr)) - sameHostID = hostSame.ID - } else { - hostSame, _ := hostRepo.Get(hostRepo.WithByAddr(req.Addr), hostRepo.WithByUser(req.User), hostRepo.WithByPort(req.Port)) - sameHostID = hostSame.ID - } - if sameHostID != 0 { - host.ID = sameHostID - upMap := make(map[string]interface{}) - upMap["name"] = req.Name - upMap["group_id"] = req.GroupID - upMap["addr"] = req.Addr - upMap["port"] = req.Port - upMap["user"] = req.User - upMap["auth_mode"] = req.AuthMode - upMap["password"] = req.Password - upMap["private_key"] = req.PrivateKey - upMap["pass_phrase"] = req.PassPhrase - upMap["remember_password"] = req.RememberPassword - upMap["description"] = req.Description - if err := hostRepo.Update(sameHostID, upMap); err != nil { - return nil, err - } - var hostinfo dto.HostInfo - if err := copier.Copy(&hostinfo, &host); err != nil { - return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - return &hostinfo, nil - } - - if err := hostRepo.Create(&host); err != nil { - return nil, err - } - var hostinfo dto.HostInfo - if err := copier.Copy(&hostinfo, &host); err != nil { - return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - return &hostinfo, nil -} - -func (u *HostService) Delete(ids []uint) error { - hosts, _ := hostRepo.GetList(commonRepo.WithIdsIn(ids)) - for _, host := range hosts { - if host.ID == 0 { - return constant.ErrRecordNotFound - } - if host.Addr == "127.0.0.1" { - return errors.New("the local connection information cannot be deleted!") - } - } - return hostRepo.Delete(commonRepo.WithIdsIn(ids)) -} - -func (u *HostService) Update(id uint, upMap map[string]interface{}) error { - return hostRepo.Update(id, upMap) -} - -func (u *HostService) EncryptHost(itemVal string) (string, error) { - privateKey, err := base64.StdEncoding.DecodeString(itemVal) - if err != nil { - return "", err - } - keyItem, err := encrypt.StringEncrypt(string(privateKey)) - return keyItem, err -} diff --git a/backend/app/service/host_tool.go b/backend/app/service/host_tool.go deleted file mode 100644 index b40bb038c..000000000 --- a/backend/app/service/host_tool.go +++ /dev/null @@ -1,565 +0,0 @@ -package service - -import ( - "bytes" - "fmt" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/app/dto/response" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/1Panel-dev/1Panel/backend/utils/ini_conf" - "github.com/1Panel-dev/1Panel/backend/utils/systemctl" - "github.com/pkg/errors" - "gopkg.in/ini.v1" - "os/exec" - "os/user" - "path" - "strconv" - "strings" -) - -type HostToolService struct{} - -type IHostToolService interface { - GetToolStatus(req request.HostToolReq) (*response.HostToolRes, error) - CreateToolConfig(req request.HostToolCreate) error - OperateTool(req request.HostToolReq) error - OperateToolConfig(req request.HostToolConfig) (*response.HostToolConfig, error) - GetToolLog(req request.HostToolLogReq) (string, error) - OperateSupervisorProcess(req request.SupervisorProcessConfig) error - GetSupervisorProcessConfig() ([]response.SupervisorProcessConfig, error) - OperateSupervisorProcessFile(req request.SupervisorProcessFileReq) (string, error) -} - -func NewIHostToolService() IHostToolService { - return &HostToolService{} -} - -func (h *HostToolService) GetToolStatus(req request.HostToolReq) (*response.HostToolRes, error) { - res := &response.HostToolRes{} - res.Type = req.Type - switch req.Type { - case constant.Supervisord: - supervisorConfig := &response.Supervisor{} - if !cmd.Which(constant.Supervisord) { - supervisorConfig.IsExist = false - res.Config = supervisorConfig - return res, nil - } - supervisorConfig.IsExist = true - serviceExist, _ := systemctl.IsExist(constant.Supervisord) - if !serviceExist { - serviceExist, _ = systemctl.IsExist(constant.Supervisor) - if !serviceExist { - supervisorConfig.IsExist = false - res.Config = supervisorConfig - return res, nil - } else { - supervisorConfig.ServiceName = constant.Supervisor - } - } else { - supervisorConfig.ServiceName = constant.Supervisord - } - - serviceNameSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName)) - if serviceNameSet.ID != 0 || serviceNameSet.Value != "" { - supervisorConfig.ServiceName = serviceNameSet.Value - } - - versionRes, _ := cmd.Exec("supervisord -v") - supervisorConfig.Version = strings.TrimSuffix(versionRes, "\n") - _, ctlRrr := exec.LookPath("supervisorctl") - supervisorConfig.CtlExist = ctlRrr == nil - - active, _ := systemctl.IsActive(supervisorConfig.ServiceName) - if active { - supervisorConfig.Status = "running" - } else { - supervisorConfig.Status = "stopped" - } - - pathSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorConfigPath)) - if pathSet.ID != 0 || pathSet.Value != "" { - supervisorConfig.ConfigPath = pathSet.Value - res.Config = supervisorConfig - return res, nil - } else { - supervisorConfig.Init = true - } - - servicePath := "/usr/lib/systemd/system/supervisor.service" - fileOp := files.NewFileOp() - if !fileOp.Stat(servicePath) { - servicePath = "/usr/lib/systemd/system/supervisord.service" - } - if fileOp.Stat(servicePath) { - startCmd, _ := ini_conf.GetIniValue(servicePath, "Service", "ExecStart") - if startCmd != "" { - args := strings.Fields(startCmd) - cIndex := -1 - for i, arg := range args { - if arg == "-c" { - cIndex = i - break - } - } - if cIndex != -1 && cIndex+1 < len(args) { - supervisorConfig.ConfigPath = args[cIndex+1] - } - } - } - if supervisorConfig.ConfigPath == "" { - configPath := "/etc/supervisord.conf" - if !fileOp.Stat(configPath) { - configPath = "/etc/supervisor/supervisord.conf" - if fileOp.Stat(configPath) { - supervisorConfig.ConfigPath = configPath - } - } - } - - res.Config = supervisorConfig - } - return res, nil -} - -func (h *HostToolService) CreateToolConfig(req request.HostToolCreate) error { - switch req.Type { - case constant.Supervisord: - fileOp := files.NewFileOp() - if !fileOp.Stat(req.ConfigPath) { - return buserr.New("ErrConfigNotFound") - } - cfg, err := ini.Load(req.ConfigPath) - if err != nil { - return err - } - service, err := cfg.GetSection("include") - if err != nil { - return err - } - targetKey, err := service.GetKey("files") - if err != nil { - return err - } - if targetKey != nil { - _, err = service.NewKey(";files", targetKey.Value()) - if err != nil { - return err - } - } - supervisorDir := path.Join(global.CONF.System.BaseDir, "1panel", "tools", "supervisord") - includeDir := path.Join(supervisorDir, "supervisor.d") - if !fileOp.Stat(includeDir) { - if err = fileOp.CreateDir(includeDir, 0755); err != nil { - return err - } - } - logDir := path.Join(supervisorDir, "log") - if !fileOp.Stat(logDir) { - if err = fileOp.CreateDir(logDir, 0755); err != nil { - return err - } - } - includePath := path.Join(includeDir, "*.ini") - targetKey.SetValue(includePath) - if err = cfg.SaveTo(req.ConfigPath); err != nil { - return err - } - - serviceNameSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName)) - if serviceNameSet.ID != 0 { - if err = settingRepo.Update(constant.SupervisorServiceName, req.ServiceName); err != nil { - return err - } - } else { - if err = settingRepo.Create(constant.SupervisorServiceName, req.ServiceName); err != nil { - return err - } - } - - configPathSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorConfigPath)) - if configPathSet.ID != 0 { - if err = settingRepo.Update(constant.SupervisorConfigPath, req.ConfigPath); err != nil { - return err - } - } else { - if err = settingRepo.Create(constant.SupervisorConfigPath, req.ConfigPath); err != nil { - return err - } - } - if err = systemctl.Restart(req.ServiceName); err != nil { - global.LOG.Errorf("[init] restart %s failed err %s", req.ServiceName, err.Error()) - return err - } - } - return nil -} - -func (h *HostToolService) OperateTool(req request.HostToolReq) error { - serviceName := req.Type - if req.Type == constant.Supervisord { - serviceNameSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName)) - if serviceNameSet.ID != 0 || serviceNameSet.Value != "" { - serviceName = serviceNameSet.Value - } - } - return systemctl.Operate(req.Operate, serviceName) -} - -func (h *HostToolService) OperateToolConfig(req request.HostToolConfig) (*response.HostToolConfig, error) { - fileOp := files.NewFileOp() - res := &response.HostToolConfig{} - configPath := "" - serviceName := "supervisord" - switch req.Type { - case constant.Supervisord: - pathSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorConfigPath)) - if pathSet.ID != 0 || pathSet.Value != "" { - configPath = pathSet.Value - } - serviceNameSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName)) - if serviceNameSet.ID != 0 || serviceNameSet.Value != "" { - serviceName = serviceNameSet.Value - } - } - switch req.Operate { - case "get": - content, err := fileOp.GetContent(configPath) - if err != nil { - return nil, err - } - res.Content = string(content) - case "set": - file, err := fileOp.OpenFile(configPath) - if err != nil { - return nil, err - } - oldContent, err := fileOp.GetContent(configPath) - if err != nil { - return nil, err - } - fileInfo, err := file.Stat() - if err != nil { - return nil, err - } - if err = fileOp.WriteFile(configPath, strings.NewReader(req.Content), fileInfo.Mode()); err != nil { - return nil, err - } - if err = systemctl.Restart(serviceName); err != nil { - _ = fileOp.WriteFile(configPath, bytes.NewReader(oldContent), fileInfo.Mode()) - return nil, err - } - } - - return res, nil -} - -func (h *HostToolService) GetToolLog(req request.HostToolLogReq) (string, error) { - fileOp := files.NewFileOp() - logfilePath := "" - switch req.Type { - case constant.Supervisord: - configPath := "/etc/supervisord.conf" - pathSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorConfigPath)) - if pathSet.ID != 0 || pathSet.Value != "" { - configPath = pathSet.Value - } - logfilePath, _ = ini_conf.GetIniValue(configPath, "supervisord", "logfile") - } - oldContent, err := fileOp.GetContent(logfilePath) - if err != nil { - return "", err - } - return string(oldContent), nil -} - -func (h *HostToolService) OperateSupervisorProcess(req request.SupervisorProcessConfig) error { - var ( - supervisordDir = path.Join(global.CONF.System.BaseDir, "1panel", "tools", "supervisord") - logDir = path.Join(supervisordDir, "log") - includeDir = path.Join(supervisordDir, "supervisor.d") - outLog = path.Join(logDir, fmt.Sprintf("%s.out.log", req.Name)) - errLog = path.Join(logDir, fmt.Sprintf("%s.err.log", req.Name)) - iniPath = path.Join(includeDir, fmt.Sprintf("%s.ini", req.Name)) - fileOp = files.NewFileOp() - ) - if req.Operate == "update" || req.Operate == "create" { - if !fileOp.Stat(req.Dir) { - return buserr.New("ErrConfigDirNotFound") - } - _, err := user.Lookup(req.User) - if err != nil { - return buserr.WithMap("ErrUserFindErr", map[string]interface{}{"name": req.User, "err": err.Error()}, err) - } - } - - switch req.Operate { - case "create": - if fileOp.Stat(iniPath) { - return buserr.New("ErrConfigAlreadyExist") - } - configFile := ini.Empty() - section, err := configFile.NewSection(fmt.Sprintf("program:%s", req.Name)) - if err != nil { - return err - } - _, _ = section.NewKey("command", strings.TrimSpace(req.Command)) - _, _ = section.NewKey("directory", req.Dir) - _, _ = section.NewKey("autorestart", "true") - _, _ = section.NewKey("startsecs", "3") - _, _ = section.NewKey("stdout_logfile", outLog) - _, _ = section.NewKey("stderr_logfile", errLog) - _, _ = section.NewKey("stdout_logfile_maxbytes", "2MB") - _, _ = section.NewKey("stderr_logfile_maxbytes", "2MB") - _, _ = section.NewKey("user", req.User) - _, _ = section.NewKey("priority", "999") - _, _ = section.NewKey("numprocs", req.Numprocs) - _, _ = section.NewKey("process_name", "%(program_name)s_%(process_num)02d") - - if err = configFile.SaveTo(iniPath); err != nil { - return err - } - if err := operateSupervisorCtl("reread", "", ""); err != nil { - return err - } - return operateSupervisorCtl("update", "", "") - case "update": - configFile, err := ini.Load(iniPath) - if err != nil { - return err - } - section, err := configFile.GetSection(fmt.Sprintf("program:%s", req.Name)) - if err != nil { - return err - } - - commandKey := section.Key("command") - commandKey.SetValue(strings.TrimSpace(req.Command)) - directoryKey := section.Key("directory") - directoryKey.SetValue(req.Dir) - userKey := section.Key("user") - userKey.SetValue(req.User) - numprocsKey := section.Key("numprocs") - numprocsKey.SetValue(req.Numprocs) - - if err = configFile.SaveTo(iniPath); err != nil { - return err - } - if err := operateSupervisorCtl("reread", "", ""); err != nil { - return err - } - return operateSupervisorCtl("update", "", "") - case "restart": - return operateSupervisorCtl("restart", req.Name, "") - case "start": - return operateSupervisorCtl("start", req.Name, "") - case "stop": - return operateSupervisorCtl("stop", req.Name, "") - case "delete": - _ = operateSupervisorCtl("remove", "", req.Name) - _ = files.NewFileOp().DeleteFile(iniPath) - _ = files.NewFileOp().DeleteFile(outLog) - _ = files.NewFileOp().DeleteFile(errLog) - if err := operateSupervisorCtl("reread", "", ""); err != nil { - return err - } - return operateSupervisorCtl("update", "", "") - } - - return nil -} - -func (h *HostToolService) GetSupervisorProcessConfig() ([]response.SupervisorProcessConfig, error) { - var ( - result []response.SupervisorProcessConfig - ) - configDir := path.Join(global.CONF.System.BaseDir, "1panel", "tools", "supervisord", "supervisor.d") - fileList, _ := NewIFileService().GetFileList(request.FileOption{FileOption: files.FileOption{Path: configDir, Expand: true, Page: 1, PageSize: 100}}) - if len(fileList.Items) == 0 { - return result, nil - } - for _, configFile := range fileList.Items { - f, err := ini.Load(configFile.Path) - if err != nil { - global.LOG.Errorf("get %s file err %s", configFile.Name, err.Error()) - continue - } - if strings.HasSuffix(configFile.Name, ".ini") { - config := response.SupervisorProcessConfig{} - name := strings.TrimSuffix(configFile.Name, ".ini") - config.Name = name - section, err := f.GetSection(fmt.Sprintf("program:%s", name)) - if err != nil { - global.LOG.Errorf("get %s file section err %s", configFile.Name, err.Error()) - continue - } - if command, _ := section.GetKey("command"); command != nil { - config.Command = command.Value() - } - if directory, _ := section.GetKey("directory"); directory != nil { - config.Dir = directory.Value() - } - if user, _ := section.GetKey("user"); user != nil { - config.User = user.Value() - } - if numprocs, _ := section.GetKey("numprocs"); numprocs != nil { - config.Numprocs = numprocs.Value() - } - _ = getProcessStatus(&config) - result = append(result, config) - } - } - return result, nil -} - -func (h *HostToolService) OperateSupervisorProcessFile(req request.SupervisorProcessFileReq) (string, error) { - var ( - fileOp = files.NewFileOp() - group = fmt.Sprintf("program:%s", req.Name) - configPath = path.Join(global.CONF.System.BaseDir, "1panel", "tools", "supervisord", "supervisor.d", fmt.Sprintf("%s.ini", req.Name)) - ) - switch req.File { - case "err.log": - logPath, err := ini_conf.GetIniValue(configPath, group, "stderr_logfile") - if err != nil { - return "", err - } - switch req.Operate { - case "get": - content, err := fileOp.GetContent(logPath) - if err != nil { - return "", err - } - return string(content), nil - case "clear": - if err = fileOp.WriteFile(logPath, strings.NewReader(""), 0755); err != nil { - return "", err - } - } - - case "out.log": - logPath, err := ini_conf.GetIniValue(configPath, group, "stdout_logfile") - if err != nil { - return "", err - } - switch req.Operate { - case "get": - content, err := fileOp.GetContent(logPath) - if err != nil { - return "", err - } - return string(content), nil - case "clear": - if err = fileOp.WriteFile(logPath, strings.NewReader(""), 0755); err != nil { - return "", err - } - } - - case "config": - switch req.Operate { - case "get": - content, err := fileOp.GetContent(configPath) - if err != nil { - return "", err - } - return string(content), nil - case "update": - if req.Content == "" { - return "", buserr.New("ErrConfigIsNull") - } - if err := fileOp.WriteFile(configPath, strings.NewReader(req.Content), 0755); err != nil { - return "", err - } - return "", operateSupervisorCtl("update", "", req.Name) - } - - } - return "", nil -} - -func operateSupervisorCtl(operate, name, group string) error { - processNames := []string{operate} - if name != "" { - includeDir := path.Join(global.CONF.System.BaseDir, "1panel", "tools", "supervisord", "supervisor.d") - f, err := ini.Load(path.Join(includeDir, fmt.Sprintf("%s.ini", name))) - if err != nil { - return err - } - section, err := f.GetSection(fmt.Sprintf("program:%s", name)) - if err != nil { - return err - } - numprocsNum := "" - if numprocs, _ := section.GetKey("numprocs"); numprocs != nil { - numprocsNum = numprocs.Value() - } - if numprocsNum == "" { - return buserr.New("ErrConfigParse") - } - processNames = append(processNames, getProcessName(name, numprocsNum)...) - } - if group != "" { - processNames = append(processNames, group) - } - - output, err := exec.Command("supervisorctl", processNames...).Output() - if err != nil { - if output != nil { - return errors.New(string(output)) - } - return err - } - return nil -} - -func getProcessName(name, numprocs string) []string { - var ( - processNames []string - ) - num, err := strconv.Atoi(numprocs) - if err != nil { - return processNames - } - if num == 1 { - processNames = append(processNames, fmt.Sprintf("%s:%s_00", name, name)) - } else { - for i := 0; i < num; i++ { - processName := fmt.Sprintf("%s:%s_0%s", name, name, strconv.Itoa(i)) - if i >= 10 { - processName = fmt.Sprintf("%s:%s_%s", name, name, strconv.Itoa(i)) - } - processNames = append(processNames, processName) - } - } - return processNames -} - -func getProcessStatus(config *response.SupervisorProcessConfig) error { - var ( - processNames = []string{"status"} - ) - processNames = append(processNames, getProcessName(config.Name, config.Numprocs)...) - output, _ := exec.Command("supervisorctl", processNames...).Output() - lines := strings.Split(string(output), "\n") - for _, line := range lines { - fields := strings.Fields(line) - if len(fields) >= 5 { - status := response.ProcessStatus{ - Name: fields[0], - Status: fields[1], - } - if fields[1] == "RUNNING" { - status.PID = strings.TrimSuffix(fields[3], ",") - status.Uptime = fields[5] - } else { - status.Msg = strings.Join(fields[2:], " ") - } - config.Status = append(config.Status, status) - } - } - return nil -} diff --git a/backend/app/service/image.go b/backend/app/service/image.go deleted file mode 100644 index 833b6652b..000000000 --- a/backend/app/service/image.go +++ /dev/null @@ -1,517 +0,0 @@ -package service - -import ( - "bufio" - "context" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "path" - "strings" - "time" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/docker" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/pkg/archive" - "github.com/docker/docker/pkg/homedir" -) - -type ImageService struct{} - -type IImageService interface { - Page(req dto.SearchWithPage) (int64, interface{}, error) - List() ([]dto.Options, error) - ListAll() ([]dto.ImageInfo, error) - ImageBuild(req dto.ImageBuild) (string, error) - ImagePull(req dto.ImagePull) (string, error) - ImageLoad(req dto.ImageLoad) error - ImageSave(req dto.ImageSave) error - ImagePush(req dto.ImagePush) (string, error) - ImageRemove(req dto.BatchDelete) error - ImageTag(req dto.ImageTag) error -} - -func NewIImageService() IImageService { - return &ImageService{} -} -func (u *ImageService) Page(req dto.SearchWithPage) (int64, interface{}, error) { - var ( - list []image.Summary - records []dto.ImageInfo - backDatas []dto.ImageInfo - ) - client, err := docker.NewDockerClient() - if err != nil { - return 0, nil, err - } - defer client.Close() - list, err = client.ImageList(context.Background(), image.ListOptions{}) - if err != nil { - return 0, nil, err - } - containers, _ := client.ContainerList(context.Background(), container.ListOptions{All: true}) - if len(req.Info) != 0 { - length, count := len(list), 0 - for count < length { - hasTag := false - for _, tag := range list[count].RepoTags { - if strings.Contains(tag, req.Info) { - hasTag = true - break - } - } - if !hasTag { - list = append(list[:count], list[(count+1):]...) - length-- - } else { - count++ - } - } - } - - for _, image := range list { - size := formatFileSize(image.Size) - records = append(records, dto.ImageInfo{ - ID: image.ID, - Tags: image.RepoTags, - IsUsed: checkUsed(image.ID, containers), - CreatedAt: time.Unix(image.Created, 0), - Size: size, - }) - } - total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize - if start > total { - backDatas = make([]dto.ImageInfo, 0) - } else { - if end >= total { - end = total - } - backDatas = records[start:end] - } - - return int64(total), backDatas, nil -} - -func (u *ImageService) ListAll() ([]dto.ImageInfo, error) { - var records []dto.ImageInfo - client, err := docker.NewDockerClient() - if err != nil { - return nil, err - } - defer client.Close() - list, err := client.ImageList(context.Background(), image.ListOptions{}) - if err != nil { - return nil, err - } - containers, _ := client.ContainerList(context.Background(), container.ListOptions{All: true}) - for _, image := range list { - size := formatFileSize(image.Size) - records = append(records, dto.ImageInfo{ - ID: image.ID, - Tags: image.RepoTags, - IsUsed: checkUsed(image.ID, containers), - CreatedAt: time.Unix(image.Created, 0), - Size: size, - }) - } - return records, nil -} - -func (u *ImageService) List() ([]dto.Options, error) { - var ( - list []image.Summary - backDatas []dto.Options - ) - client, err := docker.NewDockerClient() - if err != nil { - return nil, err - } - defer client.Close() - list, err = client.ImageList(context.Background(), image.ListOptions{}) - if err != nil { - return nil, err - } - for _, image := range list { - for _, tag := range image.RepoTags { - backDatas = append(backDatas, dto.Options{ - Option: tag, - }) - } - } - return backDatas, nil -} - -func (u *ImageService) ImageBuild(req dto.ImageBuild) (string, error) { - client, err := docker.NewDockerClient() - if err != nil { - return "", err - } - defer client.Close() - fileName := "Dockerfile" - if req.From == "edit" { - dir := fmt.Sprintf("%s/docker/build/%s", constant.DataDir, strings.ReplaceAll(req.Name, ":", "_")) - if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(dir, os.ModePerm); err != nil { - return "", err - } - } - - pathItem := fmt.Sprintf("%s/Dockerfile", dir) - file, err := os.OpenFile(pathItem, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) - if err != nil { - return "", err - } - defer file.Close() - write := bufio.NewWriter(file) - _, _ = write.WriteString(string(req.Dockerfile)) - write.Flush() - req.Dockerfile = dir - } else { - fileName = path.Base(req.Dockerfile) - req.Dockerfile = path.Dir(req.Dockerfile) - } - tar, err := archive.TarWithOptions(req.Dockerfile+"/", &archive.TarOptions{}) - if err != nil { - return "", err - } - - opts := types.ImageBuildOptions{ - Dockerfile: fileName, - Tags: []string{req.Name}, - Remove: true, - Labels: stringsToMap(req.Tags), - } - - dockerLogDir := path.Join(global.CONF.System.TmpDir, "docker_logs") - if _, err := os.Stat(dockerLogDir); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(dockerLogDir, os.ModePerm); err != nil { - return "", err - } - } - logItem := fmt.Sprintf("%s/image_build_%s_%s.log", dockerLogDir, strings.ReplaceAll(req.Name, ":", "_"), time.Now().Format(constant.DateTimeSlimLayout)) - file, err := os.OpenFile(logItem, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) - if err != nil { - return "", err - } - go func() { - defer file.Close() - defer tar.Close() - res, err := client.ImageBuild(context.Background(), tar, opts) - if err != nil { - global.LOG.Errorf("build image %s failed, err: %v", req.Name, err) - _, _ = file.WriteString("image build failed!") - return - } - defer res.Body.Close() - body, err := io.ReadAll(res.Body) - if err != nil { - global.LOG.Errorf("build image %s failed, err: %v", req.Name, err) - _, _ = file.WriteString(fmt.Sprintf("build image %s failed, err: %v", req.Name, err)) - _, _ = file.WriteString("image build failed!") - return - } - - if strings.Contains(string(body), "errorDetail") || strings.Contains(string(body), "error:") { - global.LOG.Errorf("build image %s failed", req.Name) - _, _ = file.Write(body) - _, _ = file.WriteString("image build failed!") - return - } - global.LOG.Infof("build image %s successful!", req.Name) - _, _ = file.Write(body) - _, _ = file.WriteString("image build successful!") - }() - - return path.Base(logItem), nil -} - -func (u *ImageService) ImagePull(req dto.ImagePull) (string, error) { - client, err := docker.NewDockerClient() - if err != nil { - return "", err - } - defer client.Close() - dockerLogDir := path.Join(global.CONF.System.TmpDir, "docker_logs") - if _, err := os.Stat(dockerLogDir); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(dockerLogDir, os.ModePerm); err != nil { - return "", err - } - } - imageItemName := strings.ReplaceAll(path.Base(req.ImageName), ":", "_") - logItem := fmt.Sprintf("%s/image_pull_%s_%s.log", dockerLogDir, imageItemName, time.Now().Format(constant.DateTimeSlimLayout)) - file, err := os.OpenFile(logItem, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) - if err != nil { - return "", err - } - options := image.PullOptions{} - if req.RepoID == 0 { - hasAuth, authStr := loadAuthInfo(req.ImageName) - if hasAuth { - options.RegistryAuth = authStr - } - go func() { - defer file.Close() - out, err := client.ImagePull(context.TODO(), req.ImageName, options) - if err != nil { - global.LOG.Errorf("image %s pull failed, err: %v", req.ImageName, err) - return - } - defer out.Close() - global.LOG.Infof("pull image %s successful!", req.ImageName) - _, _ = io.Copy(file, out) - }() - return path.Base(logItem), nil - } - repo, err := imageRepoRepo.Get(commonRepo.WithByID(req.RepoID)) - if err != nil { - return "", err - } - if repo.Auth { - authConfig := registry.AuthConfig{ - Username: repo.Username, - Password: repo.Password, - } - encodedJSON, err := json.Marshal(authConfig) - if err != nil { - return "", err - } - authStr := base64.URLEncoding.EncodeToString(encodedJSON) - options.RegistryAuth = authStr - } - image := repo.DownloadUrl + "/" + req.ImageName - go func() { - defer file.Close() - out, err := client.ImagePull(context.TODO(), image, options) - if err != nil { - _, _ = file.WriteString("image pull failed!") - global.LOG.Errorf("image %s pull failed, err: %v", image, err) - return - } - defer out.Close() - global.LOG.Infof("pull image %s successful!", req.ImageName) - _, _ = io.Copy(file, out) - _, _ = file.WriteString("image pull successful!") - }() - return path.Base(logItem), nil -} - -func (u *ImageService) ImageLoad(req dto.ImageLoad) error { - file, err := os.Open(req.Path) - if err != nil { - return err - } - defer file.Close() - client, err := docker.NewDockerClient() - if err != nil { - return err - } - defer client.Close() - res, err := client.ImageLoad(context.TODO(), file, true) - if err != nil { - return err - } - defer res.Body.Close() - content, err := io.ReadAll(res.Body) - if err != nil { - return err - } - if strings.Contains(string(content), "Error") { - return errors.New(string(content)) - } - return nil -} - -func (u *ImageService) ImageSave(req dto.ImageSave) error { - client, err := docker.NewDockerClient() - if err != nil { - return err - } - defer client.Close() - - out, err := client.ImageSave(context.TODO(), []string{req.TagName}) - if err != nil { - return err - } - defer out.Close() - file, err := os.OpenFile(fmt.Sprintf("%s/%s.tar", req.Path, req.Name), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666) - if err != nil { - return err - } - defer file.Close() - if _, err = io.Copy(file, out); err != nil { - return err - } - return nil -} - -func (u *ImageService) ImageTag(req dto.ImageTag) error { - client, err := docker.NewDockerClient() - if err != nil { - return err - } - defer client.Close() - - if err := client.ImageTag(context.TODO(), req.SourceID, req.TargetName); err != nil { - return err - } - return nil -} - -func (u *ImageService) ImagePush(req dto.ImagePush) (string, error) { - client, err := docker.NewDockerClient() - if err != nil { - return "", err - } - defer client.Close() - repo, err := imageRepoRepo.Get(commonRepo.WithByID(req.RepoID)) - if err != nil { - return "", err - } - options := image.PushOptions{All: true} - authConfig := registry.AuthConfig{ - Username: repo.Username, - Password: repo.Password, - } - encodedJSON, err := json.Marshal(authConfig) - if err != nil { - return "", err - } - authStr := base64.URLEncoding.EncodeToString(encodedJSON) - options.RegistryAuth = authStr - newName := fmt.Sprintf("%s/%s", repo.DownloadUrl, req.Name) - if newName != req.TagName { - if err := client.ImageTag(context.TODO(), req.TagName, newName); err != nil { - return "", err - } - } - - dockerLogDir := global.CONF.System.TmpDir + "/docker_logs" - if _, err := os.Stat(dockerLogDir); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(dockerLogDir, os.ModePerm); err != nil { - return "", err - } - } - imageItemName := strings.ReplaceAll(path.Base(req.Name), ":", "_") - logItem := fmt.Sprintf("%s/image_push_%s_%s.log", dockerLogDir, imageItemName, time.Now().Format(constant.DateTimeSlimLayout)) - file, err := os.OpenFile(logItem, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) - if err != nil { - return "", err - } - go func() { - defer file.Close() - out, err := client.ImagePush(context.TODO(), newName, options) - if err != nil { - global.LOG.Errorf("image %s push failed, err: %v", req.TagName, err) - _, _ = file.WriteString("image push failed!") - return - } - defer out.Close() - global.LOG.Infof("push image %s successful!", req.Name) - _, _ = io.Copy(file, out) - _, _ = file.WriteString("image push successful!") - }() - - return path.Base(logItem), nil -} - -func (u *ImageService) ImageRemove(req dto.BatchDelete) error { - client, err := docker.NewDockerClient() - if err != nil { - return err - } - defer client.Close() - for _, id := range req.Names { - if _, err := client.ImageRemove(context.TODO(), id, image.RemoveOptions{Force: req.Force, PruneChildren: true}); err != nil { - if strings.Contains(err.Error(), "image is being used") || strings.Contains(err.Error(), "is using") { - if strings.Contains(id, "sha256:") { - return buserr.New(constant.ErrObjectInUsed) - } - return buserr.WithDetail(constant.ErrInUsed, id, nil) - } - return err - } - } - return nil -} - -func formatFileSize(fileSize int64) (size string) { - if fileSize < 1024 { - return fmt.Sprintf("%.2fB", float64(fileSize)/float64(1)) - } else if fileSize < (1024 * 1024) { - return fmt.Sprintf("%.2fKB", float64(fileSize)/float64(1024)) - } else if fileSize < (1024 * 1024 * 1024) { - return fmt.Sprintf("%.2fMB", float64(fileSize)/float64(1024*1024)) - } else if fileSize < (1024 * 1024 * 1024 * 1024) { - return fmt.Sprintf("%.2fGB", float64(fileSize)/float64(1024*1024*1024)) - } else if fileSize < (1024 * 1024 * 1024 * 1024 * 1024) { - return fmt.Sprintf("%.2fTB", float64(fileSize)/float64(1024*1024*1024*1024)) - } else { - return fmt.Sprintf("%.2fEB", float64(fileSize)/float64(1024*1024*1024*1024*1024)) - } -} - -func checkUsed(imageID string, containers []types.Container) bool { - for _, container := range containers { - if container.ImageID == imageID { - return true - } - } - return false -} - -func loadAuthInfo(image string) (bool, string) { - if !strings.Contains(image, "/") { - return false, "" - } - homeDir := homedir.Get() - confPath := path.Join(homeDir, ".docker/config.json") - configFileBytes, err := os.ReadFile(confPath) - if err != nil { - return false, "" - } - var config dockerConfig - if err = json.Unmarshal(configFileBytes, &config); err != nil { - return false, "" - } - var ( - user string - passwd string - ) - imagePrefix := strings.Split(image, "/")[0] - if val, ok := config.Auths[imagePrefix]; ok { - itemByte, _ := base64.StdEncoding.DecodeString(val.Auth) - itemStr := string(itemByte) - if strings.Contains(itemStr, ":") { - user = strings.Split(itemStr, ":")[0] - passwd = strings.Split(itemStr, ":")[1] - } - } - authConfig := registry.AuthConfig{ - Username: user, - Password: passwd, - } - encodedJSON, err := json.Marshal(authConfig) - if err != nil { - return false, "" - } - authStr := base64.URLEncoding.EncodeToString(encodedJSON) - return true, authStr -} - -type dockerConfig struct { - Auths map[string]authConfig `json:"auths"` -} -type authConfig struct { - Auth string `json:"auth"` -} diff --git a/backend/app/service/image_repo.go b/backend/app/service/image_repo.go deleted file mode 100644 index e5dad9e90..000000000 --- a/backend/app/service/image_repo.go +++ /dev/null @@ -1,243 +0,0 @@ -package service - -import ( - "context" - "encoding/json" - "fmt" - "os" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/jinzhu/copier" - "github.com/pkg/errors" -) - -type ImageRepoService struct{} - -type IImageRepoService interface { - Page(search dto.SearchWithPage) (int64, interface{}, error) - List() ([]dto.ImageRepoOption, error) - Login(req dto.OperateByID) error - Create(req dto.ImageRepoCreate) error - Update(req dto.ImageRepoUpdate) error - BatchDelete(req dto.ImageRepoDelete) error -} - -func NewIImageRepoService() IImageRepoService { - return &ImageRepoService{} -} - -func (u *ImageRepoService) Page(req dto.SearchWithPage) (int64, interface{}, error) { - total, ops, err := imageRepoRepo.Page(req.Page, req.PageSize, commonRepo.WithLikeName(req.Info), commonRepo.WithOrderBy("created_at desc")) - var dtoOps []dto.ImageRepoInfo - for _, op := range ops { - var item dto.ImageRepoInfo - if err := copier.Copy(&item, &op); err != nil { - return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - dtoOps = append(dtoOps, item) - } - return total, dtoOps, err -} - -func (u *ImageRepoService) Login(req dto.OperateByID) error { - repo, err := imageRepoRepo.Get(commonRepo.WithByID(req.ID)) - if err != nil { - return err - } - if repo.Auth { - if err := u.CheckConn(repo.DownloadUrl, repo.Username, repo.Password); err != nil { - _ = imageRepoRepo.Update(repo.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()}) - return err - } - } - _ = imageRepoRepo.Update(repo.ID, map[string]interface{}{"status": constant.StatusSuccess}) - return nil -} - -func (u *ImageRepoService) List() ([]dto.ImageRepoOption, error) { - ops, err := imageRepoRepo.List(commonRepo.WithOrderBy("created_at desc")) - var dtoOps []dto.ImageRepoOption - for _, op := range ops { - if op.Status == constant.StatusSuccess { - var item dto.ImageRepoOption - if err := copier.Copy(&item, &op); err != nil { - return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - dtoOps = append(dtoOps, item) - } - } - return dtoOps, err -} - -func (u *ImageRepoService) Create(req dto.ImageRepoCreate) error { - if cmd.CheckIllegal(req.Username, req.Password, req.DownloadUrl) { - return buserr.New(constant.ErrCmdIllegal) - } - imageRepo, _ := imageRepoRepo.Get(commonRepo.WithByName(req.Name)) - if imageRepo.ID != 0 { - return constant.ErrRecordExist - } - if req.Protocol == "http" { - _ = u.handleRegistries(req.DownloadUrl, "", "create") - stdout, err := cmd.Exec("systemctl restart docker") - if err != nil { - return errors.New(string(stdout)) - } - ticker := time.NewTicker(3 * time.Second) - defer ticker.Stop() - ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) - if err := func() error { - for range ticker.C { - select { - case <-ctx.Done(): - cancel() - return errors.New("the docker service cannot be restarted") - default: - stdout, err := cmd.Exec("systemctl is-active docker") - if string(stdout) == "active\n" && err == nil { - global.LOG.Info("docker restart with new conf successful!") - return nil - } - } - } - return nil - }(); err != nil { - return err - } - } - - if err := copier.Copy(&imageRepo, &req); err != nil { - return errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - - imageRepo.Status = constant.StatusSuccess - if req.Auth { - if err := u.CheckConn(req.DownloadUrl, req.Username, req.Password); err != nil { - imageRepo.Status = constant.StatusFailed - imageRepo.Message = err.Error() - } - } - if err := imageRepoRepo.Create(&imageRepo); err != nil { - return err - } - - return nil -} - -func (u *ImageRepoService) BatchDelete(req dto.ImageRepoDelete) error { - for _, id := range req.Ids { - if id == 1 { - return errors.New("The default value cannot be edit !") - } - } - if err := imageRepoRepo.Delete(commonRepo.WithIdsIn(req.Ids)); err != nil { - return err - } - return nil -} - -func (u *ImageRepoService) Update(req dto.ImageRepoUpdate) error { - if req.ID == 1 { - return errors.New("The default value cannot be deleted !") - } - if cmd.CheckIllegal(req.Username, req.Password, req.DownloadUrl) { - return buserr.New(constant.ErrCmdIllegal) - } - repo, err := imageRepoRepo.Get(commonRepo.WithByID(req.ID)) - if err != nil { - return err - } - if repo.DownloadUrl != req.DownloadUrl || (!repo.Auth && req.Auth) { - _ = u.handleRegistries(req.DownloadUrl, repo.DownloadUrl, "update") - if repo.Auth { - _, _ = cmd.ExecWithCheck("docker", "logout", repo.DownloadUrl) - } - stdout, err := cmd.Exec("systemctl restart docker") - if err != nil { - return errors.New(string(stdout)) - } - } - - upMap := make(map[string]interface{}) - upMap["download_url"] = req.DownloadUrl - upMap["protocol"] = req.Protocol - upMap["username"] = req.Username - upMap["password"] = req.Password - upMap["auth"] = req.Auth - - upMap["status"] = constant.StatusSuccess - upMap["message"] = "" - if req.Auth { - if err := u.CheckConn(req.DownloadUrl, req.Username, req.Password); err != nil { - upMap["status"] = constant.StatusFailed - upMap["message"] = err.Error() - } - } - return imageRepoRepo.Update(req.ID, upMap) -} - -func (u *ImageRepoService) CheckConn(host, user, password string) error { - stdout, err := cmd.ExecWithCheck("docker", "login", "-u", user, "-p", password, host) - if err != nil { - return fmt.Errorf("stdout: %s, stderr: %v", stdout, err) - } - if strings.Contains(string(stdout), "Login Succeeded") { - return nil - } - return errors.New(string(stdout)) -} - -func (u *ImageRepoService) handleRegistries(newHost, delHost, handle string) error { - err := createIfNotExistDaemonJsonFile() - if err != nil { - return err - } - daemonMap := make(map[string]interface{}) - file, err := os.ReadFile(constant.DaemonJsonPath) - if err != nil { - return err - } - if err := json.Unmarshal(file, &daemonMap); err != nil { - return err - } - - iRegistries := daemonMap["insecure-registries"] - registries, _ := iRegistries.([]interface{}) - switch handle { - case "create": - registries = common.RemoveRepeatElement(append(registries, newHost)) - case "update": - for i, regi := range registries { - if regi == delHost { - registries = append(registries[:i], registries[i+1:]...) - } - } - registries = common.RemoveRepeatElement(append(registries, newHost)) - case "delete": - for i, regi := range registries { - if regi == delHost { - registries = append(registries[:i], registries[i+1:]...) - } - } - } - if len(registries) == 0 { - delete(daemonMap, "insecure-registries") - } else { - daemonMap["insecure-registries"] = registries - } - newJson, err := json.MarshalIndent(daemonMap, "", "\t") - if err != nil { - return err - } - if err := os.WriteFile(constant.DaemonJsonPath, newJson, 0640); err != nil { - return err - } - return nil -} diff --git a/backend/app/service/logs.go b/backend/app/service/logs.go deleted file mode 100644 index 51cdad04e..000000000 --- a/backend/app/service/logs.go +++ /dev/null @@ -1,81 +0,0 @@ -package service - -import ( - "fmt" - "os" - "path" - "path/filepath" - "sort" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/global" -) - -type LogService struct{} - -type ILogService interface { - ListSystemLogFile() ([]string, error) - LoadSystemLog(name string) (string, error) -} - -func NewILogService() ILogService { - return &LogService{} -} - -func (u *LogService) ListSystemLogFile() ([]string, error) { - logDir := path.Join(global.CONF.System.BaseDir, "1panel/log") - var files []string - if err := filepath.Walk(logDir, func(pathItem string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if !info.IsDir() && strings.HasPrefix(info.Name(), "1Panel") { - if info.Name() == "1Panel.log" { - files = append(files, time.Now().Format("2006-01-02")) - return nil - } - itemFileName := strings.TrimPrefix(info.Name(), "1Panel-") - itemFileName = strings.TrimSuffix(itemFileName, ".gz") - itemFileName = strings.TrimSuffix(itemFileName, ".log") - files = append(files, itemFileName) - return nil - } - return nil - }); err != nil { - return nil, err - } - - if len(files) < 2 { - return files, nil - } - sort.Slice(files, func(i, j int) bool { - return files[i] > files[j] - }) - - return files, nil -} - -func (u *LogService) LoadSystemLog(name string) (string, error) { - if name == time.Now().Format("2006-01-02") { - name = "1Panel.log" - } else { - name = "1Panel-" + name + ".log" - } - filePath := path.Join(global.CONF.System.DataDir, "log", name) - if _, err := os.Stat(filePath); err != nil { - fileGzPath := path.Join(global.CONF.System.DataDir, "log", name+".gz") - if _, err := os.Stat(fileGzPath); err != nil { - return "", buserr.New("ErrHttpReqNotFound") - } - if err := handleGunzip(fileGzPath); err != nil { - return "", fmt.Errorf("handle ungzip file %s failed, err: %v", fileGzPath, err) - } - } - content, err := os.ReadFile(filePath) - if err != nil { - return "", err - } - return string(content), nil -} diff --git a/backend/app/service/monitor.go b/backend/app/service/monitor.go deleted file mode 100644 index 8f907a71f..000000000 --- a/backend/app/service/monitor.go +++ /dev/null @@ -1,216 +0,0 @@ -package service - -import ( - "context" - "fmt" - "strconv" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/robfig/cron/v3" - "github.com/shirou/gopsutil/v3/cpu" - "github.com/shirou/gopsutil/v3/disk" - "github.com/shirou/gopsutil/v3/load" - "github.com/shirou/gopsutil/v3/mem" - "github.com/shirou/gopsutil/v3/net" -) - -type MonitorService struct { - DiskIO chan ([]disk.IOCountersStat) - NetIO chan ([]net.IOCountersStat) -} - -var monitorCancel context.CancelFunc - -type IMonitorService interface { - Run() - - saveIODataToDB(ctx context.Context, interval float64) - saveNetDataToDB(ctx context.Context, interval float64) -} - -func NewIMonitorService() IMonitorService { - return &MonitorService{ - DiskIO: make(chan []disk.IOCountersStat, 2), - NetIO: make(chan []net.IOCountersStat, 2), - } -} - -func (m *MonitorService) Run() { - var itemModel model.MonitorBase - totalPercent, _ := cpu.Percent(3*time.Second, false) - if len(totalPercent) == 1 { - itemModel.Cpu = totalPercent[0] - } - cpuCount, _ := cpu.Counts(false) - - loadInfo, _ := load.Avg() - itemModel.CpuLoad1 = loadInfo.Load1 - itemModel.CpuLoad5 = loadInfo.Load5 - itemModel.CpuLoad15 = loadInfo.Load15 - itemModel.LoadUsage = loadInfo.Load1 / (float64(cpuCount*2) * 0.75) * 100 - - memoryInfo, _ := mem.VirtualMemory() - itemModel.Memory = memoryInfo.UsedPercent - - if err := settingRepo.CreateMonitorBase(itemModel); err != nil { - global.LOG.Errorf("Insert basic monitoring data failed, err: %v", err) - } - - m.loadDiskIO() - m.loadNetIO() - - MonitorStoreDays, err := settingRepo.Get(settingRepo.WithByKey("MonitorStoreDays")) - if err != nil { - return - } - storeDays, _ := strconv.Atoi(MonitorStoreDays.Value) - timeForDelete := time.Now().AddDate(0, 0, -storeDays) - _ = settingRepo.DelMonitorBase(timeForDelete) - _ = settingRepo.DelMonitorIO(timeForDelete) - _ = settingRepo.DelMonitorNet(timeForDelete) -} - -func (m *MonitorService) loadDiskIO() { - ioStat, _ := disk.IOCounters() - var diskIOList []disk.IOCountersStat - for _, io := range ioStat { - diskIOList = append(diskIOList, io) - } - m.DiskIO <- diskIOList -} - -func (m *MonitorService) loadNetIO() { - netStat, _ := net.IOCounters(true) - netStatAll, _ := net.IOCounters(false) - var netList []net.IOCountersStat - netList = append(netList, netStat...) - netList = append(netList, netStatAll...) - m.NetIO <- netList -} - -func (m *MonitorService) saveIODataToDB(ctx context.Context, interval float64) { - defer close(m.DiskIO) - for { - select { - case <-ctx.Done(): - return - case ioStat := <-m.DiskIO: - select { - case <-ctx.Done(): - return - case ioStat2 := <-m.DiskIO: - var ioList []model.MonitorIO - for _, io2 := range ioStat2 { - for _, io1 := range ioStat { - if io2.Name == io1.Name { - var itemIO model.MonitorIO - itemIO.Name = io1.Name - if io2.ReadBytes != 0 && io1.ReadBytes != 0 && io2.ReadBytes > io1.ReadBytes { - itemIO.Read = uint64(float64(io2.ReadBytes-io1.ReadBytes) / interval / 60) - } - if io2.WriteBytes != 0 && io1.WriteBytes != 0 && io2.WriteBytes > io1.WriteBytes { - itemIO.Write = uint64(float64(io2.WriteBytes-io1.WriteBytes) / interval / 60) - } - - if io2.ReadCount != 0 && io1.ReadCount != 0 && io2.ReadCount > io1.ReadCount { - itemIO.Count = uint64(float64(io2.ReadCount-io1.ReadCount) / interval / 60) - } - writeCount := uint64(0) - if io2.WriteCount != 0 && io1.WriteCount != 0 && io2.WriteCount > io1.WriteCount { - writeCount = uint64(float64(io2.WriteCount-io1.WriteCount) / interval * 60) - } - if writeCount > itemIO.Count { - itemIO.Count = writeCount - } - - if io2.ReadTime != 0 && io1.ReadTime != 0 && io2.ReadTime > io1.ReadTime { - itemIO.Time = uint64(float64(io2.ReadTime-io1.ReadTime) / interval / 60) - } - writeTime := uint64(0) - if io2.WriteTime != 0 && io1.WriteTime != 0 && io2.WriteTime > io1.WriteTime { - writeTime = uint64(float64(io2.WriteTime-io1.WriteTime) / interval / 60) - } - if writeTime > itemIO.Time { - itemIO.Time = writeTime - } - ioList = append(ioList, itemIO) - break - } - } - } - if err := settingRepo.BatchCreateMonitorIO(ioList); err != nil { - global.LOG.Errorf("Insert io monitoring data failed, err: %v", err) - } - m.DiskIO <- ioStat2 - } - } - } -} - -func (m *MonitorService) saveNetDataToDB(ctx context.Context, interval float64) { - defer close(m.NetIO) - for { - select { - case <-ctx.Done(): - return - case netStat := <-m.NetIO: - select { - case <-ctx.Done(): - return - case netStat2 := <-m.NetIO: - var netList []model.MonitorNetwork - for _, net2 := range netStat2 { - for _, net1 := range netStat { - if net2.Name == net1.Name { - var itemNet model.MonitorNetwork - itemNet.Name = net1.Name - - if net2.BytesSent != 0 && net1.BytesSent != 0 && net2.BytesSent > net1.BytesSent { - itemNet.Up = float64(net2.BytesSent-net1.BytesSent) / 1024 / interval / 60 - } - if net2.BytesRecv != 0 && net1.BytesRecv != 0 && net2.BytesRecv > net1.BytesRecv { - itemNet.Down = float64(net2.BytesRecv-net1.BytesRecv) / 1024 / interval / 60 - } - netList = append(netList, itemNet) - break - } - } - } - - if err := settingRepo.BatchCreateMonitorNet(netList); err != nil { - global.LOG.Errorf("Insert network monitoring data failed, err: %v", err) - } - m.NetIO <- netStat2 - } - } - } -} - -func StartMonitor(removeBefore bool, interval string) error { - if removeBefore { - monitorCancel() - global.Cron.Remove(cron.EntryID(global.MonitorCronID)) - } - intervalItem, err := strconv.Atoi(interval) - if err != nil { - return err - } - - service := NewIMonitorService() - ctx, cancel := context.WithCancel(context.Background()) - monitorCancel = cancel - monitorID, err := global.Cron.AddJob(fmt.Sprintf("@every %sm", interval), service) - if err != nil { - return err - } - - service.Run() - - go service.saveIODataToDB(ctx, float64(intervalItem)) - go service.saveNetDataToDB(ctx, float64(intervalItem)) - - global.MonitorCronID = monitorID - return nil -} diff --git a/backend/app/service/nginx.go b/backend/app/service/nginx.go deleted file mode 100644 index 0e4ae72e3..000000000 --- a/backend/app/service/nginx.go +++ /dev/null @@ -1,146 +0,0 @@ -package service - -import ( - "fmt" - "io" - "net/http" - "os" - "path" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/utils/compose" - - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/app/dto/response" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/files" -) - -type NginxService struct { -} - -type INginxService interface { - GetNginxConfig() (*response.NginxFile, error) - GetConfigByScope(req request.NginxScopeReq) ([]response.NginxParam, error) - UpdateConfigByScope(req request.NginxConfigUpdate) error - GetStatus() (response.NginxStatus, error) - UpdateConfigFile(req request.NginxConfigFileUpdate) error - ClearProxyCache() error -} - -func NewINginxService() INginxService { - return &NginxService{} -} - -func (n NginxService) GetNginxConfig() (*response.NginxFile, error) { - nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return nil, err - } - configPath := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "conf", "nginx.conf") - byteContent, err := files.NewFileOp().GetContent(configPath) - if err != nil { - return nil, err - } - return &response.NginxFile{Content: string(byteContent)}, nil -} - -func (n NginxService) GetConfigByScope(req request.NginxScopeReq) ([]response.NginxParam, error) { - keys, ok := dto.ScopeKeyMap[req.Scope] - if !ok || len(keys) == 0 { - return nil, nil - } - return getNginxParamsByKeys(constant.NginxScopeHttp, keys, nil) -} - -func (n NginxService) UpdateConfigByScope(req request.NginxConfigUpdate) error { - keys, ok := dto.ScopeKeyMap[req.Scope] - if !ok || len(keys) == 0 { - return nil - } - return updateNginxConfig(constant.NginxScopeHttp, getNginxParams(req.Params, keys), nil) -} - -func (n NginxService) GetStatus() (response.NginxStatus, error) { - httpPort, _, err := getAppInstallPort(constant.AppOpenresty) - if err != nil { - return response.NginxStatus{}, err - } - url := "http://127.0.0.1/nginx_status" - if httpPort != 80 { - url = fmt.Sprintf("http://127.0.0.1:%v/nginx_status", httpPort) - } - res, err := http.Get(url) - if err != nil { - return response.NginxStatus{}, err - } - defer res.Body.Close() - content, err := io.ReadAll(res.Body) - if err != nil { - return response.NginxStatus{}, err - } - var status response.NginxStatus - resArray := strings.Split(string(content), " ") - status.Active = resArray[2] - status.Accepts = resArray[7] - status.Handled = resArray[8] - status.Requests = resArray[9] - status.Reading = resArray[11] - status.Writing = resArray[13] - status.Waiting = resArray[15] - return status, nil -} - -func (n NginxService) UpdateConfigFile(req request.NginxConfigFileUpdate) error { - fileOp := files.NewFileOp() - nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) - filePath := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "conf", "nginx.conf") - if err != nil { - return err - } - if req.Backup { - backupPath := path.Join(path.Dir(filePath), "bak") - if !fileOp.Stat(backupPath) { - if err := fileOp.CreateDir(backupPath, 0755); err != nil { - return err - } - } - newFile := path.Join(backupPath, "nginx.bak"+"-"+time.Now().Format("2006-01-02-15-04-05")) - if err := fileOp.Copy(filePath, backupPath); err != nil { - return err - } - if err := fileOp.Rename(path.Join(backupPath, "nginx.conf"), newFile); err != nil { - return err - } - } - oldContent, err := os.ReadFile(filePath) - if err != nil { - return err - } - if err = fileOp.WriteFile(filePath, strings.NewReader(req.Content), 0644); err != nil { - return err - } - return nginxCheckAndReload(string(oldContent), filePath, nginxInstall.ContainerName) -} - -func (n NginxService) ClearProxyCache() error { - nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return err - } - cacheDir := path.Join(nginxInstall.GetPath(), "www/common/proxy/proxy_cache_dir") - fileOp := files.NewFileOp() - if fileOp.Stat(cacheDir) { - if err = fileOp.CleanDir(cacheDir); err != nil { - return err - } - _, err = compose.Restart(nginxInstall.GetComposePath()) - if err != nil { - return err - } - } - return nil -} diff --git a/backend/app/service/nginx_utils.go b/backend/app/service/nginx_utils.go deleted file mode 100644 index a032257a9..000000000 --- a/backend/app/service/nginx_utils.go +++ /dev/null @@ -1,231 +0,0 @@ -package service - -import ( - "errors" - "fmt" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/dto/response" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/1Panel-dev/1Panel/backend/utils/nginx" - "github.com/1Panel-dev/1Panel/backend/utils/nginx/components" - "github.com/1Panel-dev/1Panel/backend/utils/nginx/parser" - "github.com/1Panel-dev/1Panel/cmd/server/nginx_conf" - "os" - "path" - "strings" - "time" -) - -func getNginxFull(website *model.Website) (dto.NginxFull, error) { - var nginxFull dto.NginxFull - nginxInstall, err := getAppInstallByKey("openresty") - if err != nil { - return nginxFull, err - } - nginxFull.Install = nginxInstall - nginxFull.Dir = path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name) - nginxFull.ConfigDir = path.Join(nginxFull.Dir, "conf") - nginxFull.ConfigFile = "nginx.conf" - nginxFull.SiteDir = path.Join(nginxFull.Dir, "www") - - var nginxConfig dto.NginxConfig - nginxConfig.FilePath = path.Join(nginxFull.Dir, "conf", "nginx.conf") - content, err := os.ReadFile(path.Join(nginxFull.ConfigDir, nginxFull.ConfigFile)) - if err != nil { - return nginxFull, err - } - config, err := parser.NewStringParser(string(content)).Parse() - if err != nil { - return dto.NginxFull{}, err - } - config.FilePath = nginxConfig.FilePath - nginxConfig.OldContent = string(content) - nginxConfig.Config = config - - nginxFull.RootConfig = nginxConfig - - if website != nil { - nginxFull.Website = *website - var siteNginxConfig dto.NginxConfig - nginxFileName := website.Alias + ".conf" - siteConfigPath := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "conf", "conf.d", nginxFileName) - siteNginxConfig.FilePath = siteConfigPath - siteNginxContent, err := os.ReadFile(siteConfigPath) - if err != nil { - return nginxFull, err - } - siteConfig, err := parser.NewStringParser(string(siteNginxContent)).Parse() - if err != nil { - return dto.NginxFull{}, err - } - siteConfig.FilePath = siteConfigPath - siteNginxConfig.Config = siteConfig - siteNginxConfig.OldContent = string(siteNginxContent) - nginxFull.SiteConfig = siteNginxConfig - } - - return nginxFull, nil -} - -func getNginxParamsByKeys(scope string, keys []string, website *model.Website) ([]response.NginxParam, error) { - nginxFull, err := getNginxFull(website) - if err != nil { - return nil, err - } - var res []response.NginxParam - var block components.IBlock - if scope == constant.NginxScopeHttp { - block = nginxFull.RootConfig.Config.FindHttp() - } else { - block = nginxFull.SiteConfig.Config.FindServers()[0] - } - for _, key := range keys { - dirs := block.FindDirectives(key) - for _, dir := range dirs { - nginxParam := response.NginxParam{ - Name: dir.GetName(), - Params: dir.GetParameters(), - } - res = append(res, nginxParam) - } - if len(dirs) == 0 { - nginxParam := response.NginxParam{ - Name: key, - Params: []string{}, - } - res = append(res, nginxParam) - } - } - return res, nil -} - -func updateNginxConfig(scope string, params []dto.NginxParam, website *model.Website) error { - nginxFull, err := getNginxFull(website) - if err != nil { - return err - } - var block components.IBlock - var config dto.NginxConfig - if scope == constant.NginxScopeHttp { - config = nginxFull.RootConfig - block = nginxFull.RootConfig.Config.FindHttp() - } else if scope == constant.NginxScopeServer { - config = nginxFull.SiteConfig - block = nginxFull.SiteConfig.Config.FindServers()[0] - } else { - config = nginxFull.SiteConfig - block = config.Config.Block - } - - for _, p := range params { - if p.UpdateScope == constant.NginxScopeOut { - config.Config.UpdateDirective(p.Name, p.Params) - } else { - block.UpdateDirective(p.Name, p.Params) - } - } - if err := nginx.WriteConfig(config.Config, nginx.IndentedStyle); err != nil { - return err - } - return nginxCheckAndReload(config.OldContent, config.FilePath, nginxFull.Install.ContainerName) -} - -func deleteNginxConfig(scope string, params []dto.NginxParam, website *model.Website) error { - nginxFull, err := getNginxFull(website) - if err != nil { - return err - } - var block components.IBlock - var config dto.NginxConfig - if scope == constant.NginxScopeHttp { - config = nginxFull.RootConfig - block = nginxFull.RootConfig.Config.FindHttp() - } else if scope == constant.NginxScopeServer { - config = nginxFull.SiteConfig - block = nginxFull.SiteConfig.Config.FindServers()[0] - } else { - config = nginxFull.SiteConfig - block = config.Config.Block - } - - for _, param := range params { - block.RemoveDirective(param.Name, param.Params) - } - - if err := nginx.WriteConfig(config.Config, nginx.IndentedStyle); err != nil { - return err - } - return nginxCheckAndReload(config.OldContent, config.FilePath, nginxFull.Install.ContainerName) -} - -func getNginxParamsFromStaticFile(scope dto.NginxKey, newParams []dto.NginxParam) []dto.NginxParam { - var ( - newConfig = &components.Config{} - err error - ) - - updateScope := "in" - switch scope { - case dto.SSL: - newConfig, err = parser.NewStringParser(string(nginx_conf.SSL)).Parse() - case dto.CACHE: - newConfig, err = parser.NewStringParser(string(nginx_conf.Cache)).Parse() - case dto.ProxyCache: - newConfig, err = parser.NewStringParser(string(nginx_conf.ProxyCache)).Parse() - } - if err != nil { - return nil - } - for _, dir := range newConfig.GetDirectives() { - addParam := dto.NginxParam{ - Name: dir.GetName(), - Params: dir.GetParameters(), - UpdateScope: updateScope, - } - isExist := false - for _, newParam := range newParams { - if newParam.Name == dir.GetName() { - if components.IsRepeatKey(newParam.Name) { - if len(newParam.Params) > 0 && newParam.Params[0] == dir.GetParameters()[0] { - isExist = true - } - } else { - isExist = true - } - } - } - if !isExist { - newParams = append(newParams, addParam) - } - } - return newParams -} - -func opNginx(containerName, operate string) error { - nginxCmd := fmt.Sprintf("docker exec -i %s %s", containerName, "nginx -s reload") - if operate == constant.NginxCheck { - nginxCmd = fmt.Sprintf("docker exec -i %s %s", containerName, "nginx -t") - } - if out, err := cmd.ExecWithTimeOut(nginxCmd, 20*time.Second); err != nil { - if out != "" { - return errors.New(out) - } - return err - } - return nil -} - -func nginxCheckAndReload(oldContent string, filePath string, containerName string) error { - if err := opNginx(containerName, constant.NginxCheck); err != nil { - _ = files.NewFileOp().WriteFile(filePath, strings.NewReader(oldContent), 0644) - return err - } - if err := opNginx(containerName, constant.NginxReload); err != nil { - _ = files.NewFileOp().WriteFile(filePath, strings.NewReader(oldContent), 0644) - return err - } - return nil -} diff --git a/backend/app/service/php_extensions.go b/backend/app/service/php_extensions.go deleted file mode 100644 index adecc15e5..000000000 --- a/backend/app/service/php_extensions.go +++ /dev/null @@ -1,86 +0,0 @@ -package service - -import ( - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/app/dto/response" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" -) - -type PHPExtensionsService struct { -} - -type IPHPExtensionsService interface { - Page(req request.PHPExtensionsSearch) (int64, []response.PHPExtensionsDTO, error) - List() ([]response.PHPExtensionsDTO, error) - Create(req request.PHPExtensionsCreate) error - Update(req request.PHPExtensionsUpdate) error - Delete(req request.PHPExtensionsDelete) error -} - -func NewIPHPExtensionsService() IPHPExtensionsService { - return &PHPExtensionsService{} -} - -func (p PHPExtensionsService) Page(req request.PHPExtensionsSearch) (int64, []response.PHPExtensionsDTO, error) { - var ( - total int64 - extensions []model.PHPExtensions - err error - result []response.PHPExtensionsDTO - ) - total, extensions, err = phpExtensionsRepo.Page(req.Page, req.PageSize) - if err != nil { - return 0, nil, err - } - for _, extension := range extensions { - result = append(result, response.PHPExtensionsDTO{ - PHPExtensions: extension, - }) - } - return total, result, nil -} - -func (p PHPExtensionsService) List() ([]response.PHPExtensionsDTO, error) { - var ( - extensions []model.PHPExtensions - err error - result []response.PHPExtensionsDTO - ) - extensions, err = phpExtensionsRepo.List() - if err != nil { - return nil, err - } - for _, extension := range extensions { - result = append(result, response.PHPExtensionsDTO{ - PHPExtensions: extension, - }) - } - return result, nil -} - -func (p PHPExtensionsService) Create(req request.PHPExtensionsCreate) error { - exist, _ := phpExtensionsRepo.GetFirst(commonRepo.WithByName(req.Name)) - if exist.ID > 0 { - return buserr.New(constant.ErrNameIsExist) - } - extension := model.PHPExtensions{ - Name: req.Name, - Extensions: req.Extensions, - } - return phpExtensionsRepo.Create(&extension) -} - -func (p PHPExtensionsService) Update(req request.PHPExtensionsUpdate) error { - exist, err := phpExtensionsRepo.GetFirst(commonRepo.WithByID(req.ID)) - if err != nil { - return err - } - exist.Extensions = req.Extensions - return phpExtensionsRepo.Save(&exist) -} - -func (p PHPExtensionsService) Delete(req request.PHPExtensionsDelete) error { - return phpExtensionsRepo.DeleteBy(commonRepo.WithByID(req.ID)) -} diff --git a/backend/app/service/process.go b/backend/app/service/process.go deleted file mode 100644 index 494b40282..000000000 --- a/backend/app/service/process.go +++ /dev/null @@ -1,27 +0,0 @@ -package service - -import ( - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/shirou/gopsutil/v3/process" -) - -type ProcessService struct{} - -type IProcessService interface { - StopProcess(req request.ProcessReq) error -} - -func NewIProcessService() IProcessService { - return &ProcessService{} -} - -func (p *ProcessService) StopProcess(req request.ProcessReq) error { - proc, err := process.NewProcess(req.PID) - if err != nil { - return err - } - if err := proc.Kill(); err != nil { - return err - } - return nil -} diff --git a/backend/app/service/recycle_bin.go b/backend/app/service/recycle_bin.go deleted file mode 100644 index 02ce52106..000000000 --- a/backend/app/service/recycle_bin.go +++ /dev/null @@ -1,212 +0,0 @@ -package service - -import ( - "fmt" - "math" - "os" - "path" - "regexp" - "strconv" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/app/dto/response" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/shirou/gopsutil/v3/disk" -) - -type RecycleBinService struct { -} - -type IRecycleBinService interface { - Page(search dto.PageInfo) (int64, []response.RecycleBinDTO, error) - Create(create request.RecycleBinCreate) error - Reduce(reduce request.RecycleBinReduce) error - Clear() error -} - -func NewIRecycleBinService() IRecycleBinService { - return &RecycleBinService{} -} - -func (r RecycleBinService) Page(search dto.PageInfo) (int64, []response.RecycleBinDTO, error) { - var ( - result []response.RecycleBinDTO - ) - partitions, err := disk.Partitions(false) - if err != nil { - return 0, nil, err - } - op := files.NewFileOp() - for _, p := range partitions { - dir := path.Join(p.Mountpoint, ".1panel_clash") - if !op.Stat(dir) { - continue - } - clashFiles, err := os.ReadDir(dir) - if err != nil { - return 0, nil, err - } - for _, file := range clashFiles { - if strings.HasPrefix(file.Name(), "_1p_") { - recycleDTO, err := getRecycleBinDTOFromName(file.Name()) - recycleDTO.IsDir = file.IsDir() - recycleDTO.From = dir - if err == nil { - result = append(result, *recycleDTO) - } - } - } - } - startIndex := (search.Page - 1) * search.PageSize - endIndex := startIndex + search.PageSize - - if startIndex > len(result) { - return int64(len(result)), result, nil - } - if endIndex > len(result) { - endIndex = len(result) - } - return int64(len(result)), result[startIndex:endIndex], nil -} - -func (r RecycleBinService) Create(create request.RecycleBinCreate) error { - op := files.NewFileOp() - if !op.Stat(create.SourcePath) { - return buserr.New(constant.ErrLinkPathNotFound) - } - clashDir, err := getClashDir(create.SourcePath) - if err != nil { - return err - } - paths := strings.Split(create.SourcePath, "/") - rNamePre := strings.Join(paths, "_1p_") - deleteTime := time.Now() - openFile, err := op.OpenFile(create.SourcePath) - if err != nil { - return err - } - fileInfo, err := openFile.Stat() - if err != nil { - return err - } - size := 0 - if fileInfo.IsDir() { - sizeF, err := op.GetDirSize(create.SourcePath) - if err != nil { - return err - } - size = int(sizeF) - } else { - size = int(fileInfo.Size()) - } - - rName := fmt.Sprintf("_1p_%s%s_p_%d_%d", "file", rNamePre, size, deleteTime.Unix()) - return op.Mv(create.SourcePath, path.Join(clashDir, rName)) -} - -func (r RecycleBinService) Reduce(reduce request.RecycleBinReduce) error { - filePath := path.Join(reduce.From, reduce.RName) - op := files.NewFileOp() - if !op.Stat(filePath) { - return buserr.New(constant.ErrLinkPathNotFound) - } - recycleBinDTO, err := getRecycleBinDTOFromName(reduce.RName) - if err != nil { - return err - } - if !op.Stat(path.Dir(recycleBinDTO.SourcePath)) { - return buserr.New("ErrSourcePathNotFound") - } - if op.Stat(recycleBinDTO.SourcePath) { - if err = op.RmRf(recycleBinDTO.SourcePath); err != nil { - return err - } - } - return op.Mv(filePath, recycleBinDTO.SourcePath) -} - -func (r RecycleBinService) Clear() error { - partitions, err := disk.Partitions(false) - if err != nil { - return err - } - op := files.NewFileOp() - for _, p := range partitions { - dir := path.Join(p.Mountpoint, ".1panel_clash") - if !op.Stat(dir) { - continue - } - newDir := path.Join(p.Mountpoint, "1panel_clash") - if err := op.Mv(dir, newDir); err != nil { - return err - } - go func() { - _ = op.DeleteDir(newDir) - }() - } - return nil -} - -func getClashDir(realPath string) (string, error) { - partitions, err := disk.Partitions(false) - if err != nil { - return "", err - } - for _, p := range partitions { - if p.Mountpoint == "/" { - continue - } - if strings.HasPrefix(realPath, p.Mountpoint) { - clashDir := path.Join(p.Mountpoint, ".1panel_clash") - if err = createClashDir(path.Join(p.Mountpoint, ".1panel_clash")); err != nil { - return "", err - } - return clashDir, nil - } - } - return constant.RecycleBinDir, createClashDir(constant.RecycleBinDir) -} - -func createClashDir(clashDir string) error { - op := files.NewFileOp() - if !op.Stat(clashDir) { - if err := op.CreateDir(clashDir, 0755); err != nil { - return err - } - } - return nil -} - -func getRecycleBinDTOFromName(filename string) (*response.RecycleBinDTO, error) { - r := regexp.MustCompile(`_1p_file_1p_(.+)_p_(\d+)_(\d+)`) - matches := r.FindStringSubmatch(filename) - if len(matches) != 4 { - return nil, fmt.Errorf("invalid filename format") - } - sourcePath := "/" + strings.ReplaceAll(matches[1], "_1p_", "/") - size, err := strconv.ParseInt(matches[2], 10, 64) - if err != nil { - return nil, err - } - if size < math.MinInt || size > math.MaxInt { - return nil, fmt.Errorf("size out of int range") - } - - deleteTime, err := strconv.ParseInt(matches[3], 10, 64) - if err != nil { - return nil, err - } - return &response.RecycleBinDTO{ - Name: path.Base(sourcePath), - Size: int(size), - Type: "file", - DeleteTime: time.Unix(deleteTime, 0), - SourcePath: sourcePath, - RName: filename, - }, nil -} diff --git a/backend/app/service/runtime.go b/backend/app/service/runtime.go deleted file mode 100644 index 342ab5dad..000000000 --- a/backend/app/service/runtime.go +++ /dev/null @@ -1,618 +0,0 @@ -package service - -import ( - "context" - "encoding/json" - "fmt" - "os" - "path" - "path/filepath" - "regexp" - "strconv" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/app/dto/response" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/app/repo" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - cmd2 "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/compose" - "github.com/1Panel-dev/1Panel/backend/utils/docker" - "github.com/1Panel-dev/1Panel/backend/utils/env" - "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/pkg/errors" - "github.com/subosito/gotenv" -) - -type RuntimeService struct { -} - -type IRuntimeService interface { - Page(req request.RuntimeSearch) (int64, []response.RuntimeDTO, error) - Create(create request.RuntimeCreate) (*model.Runtime, error) - Delete(delete request.RuntimeDelete) error - Update(req request.RuntimeUpdate) error - Get(id uint) (res *response.RuntimeDTO, err error) - GetNodePackageRunScript(req request.NodePackageReq) ([]response.PackageScripts, error) - OperateRuntime(req request.RuntimeOperate) error - GetNodeModules(req request.NodeModuleReq) ([]response.NodeModule, error) - OperateNodeModules(req request.NodeModuleOperateReq) error - SyncForRestart() error - SyncRuntimeStatus() error - DeleteCheck(installID uint) ([]dto.AppResource, error) -} - -func NewRuntimeService() IRuntimeService { - return &RuntimeService{} -} - -func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, error) { - var ( - opts []repo.DBOption - ) - if create.Name != "" { - opts = append(opts, commonRepo.WithLikeName(create.Name)) - } - if create.Type != "" { - opts = append(opts, commonRepo.WithByType(create.Type)) - } - exist, _ := runtimeRepo.GetFirst(opts...) - if exist != nil { - return nil, buserr.New(constant.ErrNameIsExist) - } - fileOp := files.NewFileOp() - - switch create.Type { - case constant.RuntimePHP: - if create.Resource == constant.ResourceLocal { - runtime := &model.Runtime{ - Name: create.Name, - Resource: create.Resource, - Type: create.Type, - Version: create.Version, - Status: constant.RuntimeNormal, - } - return nil, runtimeRepo.Create(context.Background(), runtime) - } - exist, _ = runtimeRepo.GetFirst(runtimeRepo.WithImage(create.Image)) - if exist != nil { - return nil, buserr.New(constant.ErrImageExist) - } - case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo: - if !fileOp.Stat(create.CodeDir) { - return nil, buserr.New(constant.ErrPathNotFound) - } - create.Install = true - if err := checkPortExist(create.Port); err != nil { - return nil, err - } - for _, export := range create.ExposedPorts { - if err := checkPortExist(export.HostPort); err != nil { - return nil, err - } - } - if containerName, ok := create.Params["CONTAINER_NAME"]; ok { - if err := checkContainerName(containerName.(string)); err != nil { - return nil, err - } - } - } - - appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(create.AppDetailID)) - if err != nil { - return nil, err - } - app, err := appRepo.GetFirst(commonRepo.WithByID(appDetail.AppId)) - if err != nil { - return nil, err - } - - appVersionDir := filepath.Join(app.GetAppResourcePath(), appDetail.Version) - if !fileOp.Stat(appVersionDir) || appDetail.Update { - if err = downloadApp(app, appDetail, nil); err != nil { - return nil, err - } - } - - runtime := &model.Runtime{ - Name: create.Name, - AppDetailID: create.AppDetailID, - Type: create.Type, - Image: create.Image, - Resource: create.Resource, - Version: create.Version, - } - - switch create.Type { - case constant.RuntimePHP: - if err = handlePHP(create, runtime, fileOp, appVersionDir); err != nil { - return nil, err - } - case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo: - runtime.Port = create.Port - if err = handleNodeAndJava(create, runtime, fileOp, appVersionDir); err != nil { - return nil, err - } - } - if err := runtimeRepo.Create(context.Background(), runtime); err != nil { - return nil, err - } - return runtime, nil -} - -func (r *RuntimeService) Page(req request.RuntimeSearch) (int64, []response.RuntimeDTO, error) { - var ( - opts []repo.DBOption - res []response.RuntimeDTO - ) - if req.Name != "" { - opts = append(opts, commonRepo.WithLikeName(req.Name)) - } - if req.Status != "" { - opts = append(opts, runtimeRepo.WithStatus(req.Status)) - } - if req.Type != "" { - opts = append(opts, commonRepo.WithByType(req.Type)) - } - total, runtimes, err := runtimeRepo.Page(req.Page, req.PageSize, opts...) - if err != nil { - return 0, nil, err - } - for _, runtime := range runtimes { - runtimeDTO := response.NewRuntimeDTO(runtime) - runtimeDTO.Params = make(map[string]interface{}) - envs, err := gotenv.Unmarshal(runtime.Env) - if err != nil { - return 0, nil, err - } - for k, v := range envs { - runtimeDTO.Params[k] = v - } - res = append(res, runtimeDTO) - } - return total, res, nil -} - -func (r *RuntimeService) DeleteCheck(runTimeId uint) ([]dto.AppResource, error) { - var res []dto.AppResource - websites, _ := websiteRepo.GetBy(websiteRepo.WithRuntimeID(runTimeId)) - for _, website := range websites { - res = append(res, dto.AppResource{ - Type: "website", - Name: website.PrimaryDomain, - }) - } - return res, nil -} - -func (r *RuntimeService) Delete(runtimeDelete request.RuntimeDelete) error { - runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(runtimeDelete.ID)) - if err != nil { - return err - } - website, _ := websiteRepo.GetFirst(websiteRepo.WithRuntimeID(runtimeDelete.ID)) - if website.ID > 0 { - return buserr.New(constant.ErrDelWithWebsite) - } - if runtime.Resource == constant.ResourceAppstore { - projectDir := runtime.GetPath() - switch runtime.Type { - case constant.RuntimePHP: - client, err := docker.NewClient() - if err != nil { - return err - } - defer client.Close() - imageID, err := client.GetImageIDByName(runtime.Image) - if err != nil { - return err - } - if imageID != "" { - if err := client.DeleteImage(imageID); err != nil { - global.LOG.Errorf("delete image id [%s] error %v", imageID, err) - } - } - case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo: - if out, err := compose.Down(runtime.GetComposePath()); err != nil && !runtimeDelete.ForceDelete { - if out != "" { - return errors.New(out) - } - return err - } - } - if err := files.NewFileOp().DeleteDir(projectDir); err != nil && !runtimeDelete.ForceDelete { - return err - } - } - return runtimeRepo.DeleteBy(commonRepo.WithByID(runtimeDelete.ID)) -} - -func (r *RuntimeService) Get(id uint) (*response.RuntimeDTO, error) { - runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(id)) - if err != nil { - return nil, err - } - - res := response.NewRuntimeDTO(*runtime) - if runtime.Resource == constant.ResourceLocal { - return &res, nil - } - appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(runtime.AppDetailID)) - if err != nil { - return nil, err - } - res.AppID = appDetail.AppId - switch runtime.Type { - case constant.RuntimePHP: - var ( - appForm dto.AppForm - appParams []response.AppParam - ) - if err := json.Unmarshal([]byte(runtime.Params), &appForm); err != nil { - return nil, err - } - envs, err := gotenv.Unmarshal(runtime.Env) - if err != nil { - return nil, err - } - if v, ok := envs["CONTAINER_PACKAGE_URL"]; ok { - res.Source = v - } - for _, form := range appForm.FormFields { - if v, ok := envs[form.EnvKey]; ok { - appParam := response.AppParam{ - Edit: false, - Key: form.EnvKey, - Rule: form.Rule, - Type: form.Type, - Required: form.Required, - } - if form.Edit { - appParam.Edit = true - } - appParam.LabelZh = form.LabelZh - appParam.LabelEn = form.LabelEn - appParam.Multiple = form.Multiple - appParam.Value = v - if form.Type == "select" { - if form.Multiple { - if v == "" { - appParam.Value = []string{} - } else { - appParam.Value = strings.Split(v, ",") - } - } else { - for _, fv := range form.Values { - if fv.Value == v { - appParam.ShowValue = fv.Label - break - } - } - } - appParam.Values = form.Values - } - appParams = append(appParams, appParam) - } - } - res.AppParams = appParams - case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo: - res.Params = make(map[string]interface{}) - envs, err := gotenv.Unmarshal(runtime.Env) - if err != nil { - return nil, err - } - for k, v := range envs { - switch k { - case "NODE_APP_PORT", "PANEL_APP_PORT_HTTP", "JAVA_APP_PORT", "GO_APP_PORT": - port, err := strconv.Atoi(v) - if err != nil { - return nil, err - } - res.Params[k] = port - default: - if strings.Contains(k, "CONTAINER_PORT") || strings.Contains(k, "HOST_PORT") { - if strings.Contains(k, "CONTAINER_PORT") { - r := regexp.MustCompile(`_(\d+)$`) - matches := r.FindStringSubmatch(k) - containerPort, err := strconv.Atoi(v) - if err != nil { - return nil, err - } - hostPort, err := strconv.Atoi(envs[fmt.Sprintf("HOST_PORT_%s", matches[1])]) - if err != nil { - return nil, err - } - res.ExposedPorts = append(res.ExposedPorts, request.ExposedPort{ - ContainerPort: containerPort, - HostPort: hostPort, - }) - } - } else { - res.Params[k] = v - } - } - } - if v, ok := envs["CONTAINER_PACKAGE_URL"]; ok { - res.Source = v - } - } - - return &res, nil -} - -func (r *RuntimeService) Update(req request.RuntimeUpdate) error { - runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID)) - if err != nil { - return err - } - if runtime.Resource == constant.ResourceLocal { - runtime.Version = req.Version - return runtimeRepo.Save(runtime) - } - oldImage := runtime.Image - switch runtime.Type { - case constant.RuntimePHP: - exist, _ := runtimeRepo.GetFirst(runtimeRepo.WithImage(req.Name), runtimeRepo.WithNotId(req.ID)) - if exist != nil { - return buserr.New(constant.ErrImageExist) - } - case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo: - if runtime.Port != req.Port { - if err = checkPortExist(req.Port); err != nil { - return err - } - runtime.Port = req.Port - } - for _, export := range req.ExposedPorts { - if err = checkPortExist(export.HostPort); err != nil { - return err - } - } - if containerName, ok := req.Params["CONTAINER_NAME"]; ok { - envs, err := gotenv.Unmarshal(runtime.Env) - if err != nil { - return err - } - oldContainerName := envs["CONTAINER_NAME"] - if containerName != oldContainerName { - if err := checkContainerName(containerName.(string)); err != nil { - return err - } - } - } - - appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(runtime.AppDetailID)) - if err != nil { - return err - } - app, err := appRepo.GetFirst(commonRepo.WithByID(appDetail.AppId)) - if err != nil { - return err - } - fileOp := files.NewFileOp() - appVersionDir := path.Join(constant.AppResourceDir, app.Resource, app.Key, appDetail.Version) - if !fileOp.Stat(appVersionDir) || appDetail.Update { - if err := downloadApp(app, appDetail, nil); err != nil { - return err - } - _ = fileOp.Rename(path.Join(runtime.GetPath(), "run.sh"), path.Join(runtime.GetPath(), "run.sh.bak")) - _ = fileOp.CopyFile(path.Join(appVersionDir, "run.sh"), runtime.GetPath()) - } - } - - projectDir := path.Join(constant.RuntimeDir, runtime.Type, runtime.Name) - create := request.RuntimeCreate{ - Image: req.Image, - Type: runtime.Type, - Source: req.Source, - Params: req.Params, - CodeDir: req.CodeDir, - Version: req.Version, - NodeConfig: request.NodeConfig{ - Port: req.Port, - Install: true, - ExposedPorts: req.ExposedPorts, - }, - } - composeContent, envContent, _, err := handleParams(create, projectDir) - if err != nil { - return err - } - runtime.Env = string(envContent) - runtime.DockerCompose = string(composeContent) - - switch runtime.Type { - case constant.RuntimePHP: - runtime.Image = req.Image - runtime.Status = constant.RuntimeBuildIng - _ = runtimeRepo.Save(runtime) - client, err := docker.NewClient() - if err != nil { - return err - } - defer client.Close() - imageID, err := client.GetImageIDByName(oldImage) - if err != nil { - return err - } - go buildRuntime(runtime, imageID, req.Rebuild) - case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo: - runtime.Version = req.Version - runtime.CodeDir = req.CodeDir - runtime.Port = req.Port - runtime.Status = constant.RuntimeReCreating - _ = runtimeRepo.Save(runtime) - go reCreateRuntime(runtime) - } - return nil -} - -func (r *RuntimeService) GetNodePackageRunScript(req request.NodePackageReq) ([]response.PackageScripts, error) { - fileOp := files.NewFileOp() - if !fileOp.Stat(req.CodeDir) { - return nil, buserr.New(constant.ErrPathNotFound) - } - if !fileOp.Stat(path.Join(req.CodeDir, "package.json")) { - return nil, buserr.New(constant.ErrPackageJsonNotFound) - } - content, err := fileOp.GetContent(path.Join(req.CodeDir, "package.json")) - if err != nil { - return nil, err - } - var packageMap map[string]interface{} - err = json.Unmarshal(content, &packageMap) - if err != nil { - return nil, err - } - scripts, ok := packageMap["scripts"] - if !ok { - return nil, buserr.New(constant.ErrScriptsNotFound) - } - var packageScripts []response.PackageScripts - for k, v := range scripts.(map[string]interface{}) { - packageScripts = append(packageScripts, response.PackageScripts{ - Name: k, - Script: v.(string), - }) - } - return packageScripts, nil -} - -func (r *RuntimeService) OperateRuntime(req request.RuntimeOperate) error { - runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID)) - if err != nil { - return err - } - defer func() { - if err != nil { - runtime.Status = constant.RuntimeError - runtime.Message = err.Error() - _ = runtimeRepo.Save(runtime) - } - }() - switch req.Operate { - case constant.RuntimeUp: - if err = runComposeCmdWithLog(req.Operate, runtime.GetComposePath(), runtime.GetLogPath()); err != nil { - return err - } - if err = SyncRuntimeContainerStatus(runtime); err != nil { - return err - } - case constant.RuntimeDown: - if err = runComposeCmdWithLog(req.Operate, runtime.GetComposePath(), runtime.GetLogPath()); err != nil { - return err - } - runtime.Status = constant.RuntimeStopped - case constant.RuntimeRestart: - if err = runComposeCmdWithLog(constant.RuntimeDown, runtime.GetComposePath(), runtime.GetLogPath()); err != nil { - return err - } - if err = runComposeCmdWithLog(constant.RuntimeUp, runtime.GetComposePath(), runtime.GetLogPath()); err != nil { - return err - } - if err = SyncRuntimeContainerStatus(runtime); err != nil { - return err - } - } - return runtimeRepo.Save(runtime) -} - -func (r *RuntimeService) GetNodeModules(req request.NodeModuleReq) ([]response.NodeModule, error) { - runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID)) - if err != nil { - return nil, err - } - var res []response.NodeModule - nodeModulesPath := path.Join(runtime.CodeDir, "node_modules") - fileOp := files.NewFileOp() - if !fileOp.Stat(nodeModulesPath) { - return nil, buserr.New("ErrNodeModulesNotFound") - } - moduleDirs, err := os.ReadDir(nodeModulesPath) - if err != nil { - return nil, err - } - for _, moduleDir := range moduleDirs { - packagePath := path.Join(nodeModulesPath, moduleDir.Name(), "package.json") - if !fileOp.Stat(packagePath) { - continue - } - content, err := fileOp.GetContent(packagePath) - if err != nil { - continue - } - module := response.NodeModule{} - if err := json.Unmarshal(content, &module); err != nil { - continue - } - res = append(res, module) - } - return res, nil -} - -func (r *RuntimeService) OperateNodeModules(req request.NodeModuleOperateReq) error { - runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID)) - if err != nil { - return err - } - containerName, err := env.GetEnvValueByKey(runtime.GetEnvPath(), "CONTAINER_NAME") - if err != nil { - return err - } - cmd := req.PkgManager - switch req.Operate { - case constant.RuntimeInstall: - if req.PkgManager == constant.RuntimeNpm { - cmd += " install" - } else { - cmd += " add" - } - case constant.RuntimeUninstall: - if req.PkgManager == constant.RuntimeNpm { - cmd += " uninstall" - } else { - cmd += " remove" - } - case constant.RuntimeUpdate: - if req.PkgManager == constant.RuntimeNpm { - cmd += " update" - } else { - cmd += " upgrade" - } - } - cmd += " " + req.Module - return cmd2.ExecContainerScript(containerName, cmd, 5*time.Minute) -} - -func (r *RuntimeService) SyncForRestart() error { - runtimes, err := runtimeRepo.List() - if err != nil { - return err - } - for _, runtime := range runtimes { - if runtime.Status == constant.RuntimeBuildIng || runtime.Status == constant.RuntimeReCreating || runtime.Status == constant.RuntimeStarting || runtime.Status == constant.RuntimeCreating { - runtime.Status = constant.SystemRestart - runtime.Message = "System restart causing interrupt" - _ = runtimeRepo.Save(&runtime) - } - } - return nil -} - -func (r *RuntimeService) SyncRuntimeStatus() error { - runtimes, err := runtimeRepo.List() - if err != nil { - return err - } - for _, runtime := range runtimes { - if runtime.Type == constant.RuntimeNode || runtime.Type == constant.RuntimeJava || runtime.Type == constant.RuntimeGo { - _ = SyncRuntimeContainerStatus(&runtime) - } - } - return nil -} diff --git a/backend/app/service/runtime_utils.go b/backend/app/service/runtime_utils.go deleted file mode 100644 index acebcf24d..000000000 --- a/backend/app/service/runtime_utils.go +++ /dev/null @@ -1,436 +0,0 @@ -package service - -import ( - "bytes" - "fmt" - "io" - "net/http" - "os" - "os/exec" - "path" - "path/filepath" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/docker" - "github.com/1Panel-dev/1Panel/backend/utils/files" - httpUtil "github.com/1Panel-dev/1Panel/backend/utils/http" - "github.com/pkg/errors" - "github.com/subosito/gotenv" - "gopkg.in/yaml.v3" -) - -func handleNodeAndJava(create request.RuntimeCreate, runtime *model.Runtime, fileOp files.FileOp, appVersionDir string) (err error) { - runtimeDir := path.Join(constant.RuntimeDir, create.Type) - if err = fileOp.CopyDir(appVersionDir, runtimeDir); err != nil { - return - } - versionDir := path.Join(runtimeDir, filepath.Base(appVersionDir)) - projectDir := path.Join(runtimeDir, create.Name) - defer func() { - if err != nil { - _ = fileOp.DeleteDir(projectDir) - } - }() - if err = fileOp.Rename(versionDir, projectDir); err != nil { - return - } - composeContent, envContent, _, err := handleParams(create, projectDir) - if err != nil { - return - } - runtime.DockerCompose = string(composeContent) - runtime.Env = string(envContent) - runtime.Status = constant.RuntimeCreating - runtime.CodeDir = create.CodeDir - - nodeDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(runtime.AppDetailID)) - if err != nil { - return err - } - - go func() { - if _, _, err := httpUtil.HandleGet(nodeDetail.DownloadCallBackUrl, http.MethodGet, constant.TimeOut5s); err != nil { - global.LOG.Errorf("http request failed(handleNode), err: %v", err) - return - } - }() - go startRuntime(runtime) - - return -} - -func handlePHP(create request.RuntimeCreate, runtime *model.Runtime, fileOp files.FileOp, appVersionDir string) (err error) { - buildDir := path.Join(appVersionDir, "build") - if !fileOp.Stat(buildDir) { - return buserr.New(constant.ErrDirNotFound) - } - runtimeDir := path.Join(constant.RuntimeDir, create.Type) - tempDir := filepath.Join(runtimeDir, fmt.Sprintf("%d", time.Now().UnixNano())) - if err = fileOp.CopyDir(buildDir, tempDir); err != nil { - return - } - oldDir := path.Join(tempDir, "build") - projectDir := path.Join(runtimeDir, create.Name) - defer func() { - if err != nil { - _ = fileOp.DeleteDir(projectDir) - } - }() - if oldDir != projectDir { - if err = fileOp.Rename(oldDir, projectDir); err != nil { - return - } - if err = fileOp.DeleteDir(tempDir); err != nil { - return - } - } - composeContent, envContent, forms, err := handleParams(create, projectDir) - if err != nil { - return - } - runtime.DockerCompose = string(composeContent) - runtime.Env = string(envContent) - runtime.Params = string(forms) - runtime.Status = constant.RuntimeBuildIng - - go buildRuntime(runtime, "", false) - return -} - -func startRuntime(runtime *model.Runtime) { - if err := runComposeCmdWithLog("up", runtime.GetComposePath(), runtime.GetLogPath()); err != nil { - runtime.Status = constant.RuntimeError - runtime.Message = err.Error() - _ = runtimeRepo.Save(runtime) - return - } - - if err := SyncRuntimeContainerStatus(runtime); err != nil { - runtime.Status = constant.RuntimeError - runtime.Message = err.Error() - _ = runtimeRepo.Save(runtime) - return - } -} - -func reCreateRuntime(runtime *model.Runtime) { - var err error - defer func() { - if err != nil { - runtime.Status = constant.RuntimeError - runtime.Message = err.Error() - _ = runtimeRepo.Save(runtime) - } - }() - if err = runComposeCmdWithLog("down", runtime.GetComposePath(), runtime.GetLogPath()); err != nil { - return - } - if err = runComposeCmdWithLog("up", runtime.GetComposePath(), runtime.GetLogPath()); err != nil { - return - } - if err := SyncRuntimeContainerStatus(runtime); err != nil { - return - } -} - -func runComposeCmdWithLog(operate string, composePath string, logPath string) error { - cmd := exec.Command("docker-compose", "-f", composePath, operate) - if operate == "up" { - cmd = exec.Command("docker-compose", "-f", composePath, operate, "-d") - } - logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666) - if err != nil { - global.LOG.Errorf("Failed to open log file: %v", err) - return err - } - defer logFile.Close() - multiWriterStdout := io.MultiWriter(os.Stdout, logFile) - cmd.Stdout = multiWriterStdout - var stderrBuf bytes.Buffer - multiWriterStderr := io.MultiWriter(&stderrBuf, logFile, os.Stderr) - cmd.Stderr = multiWriterStderr - - err = cmd.Run() - if err != nil { - return errors.New(buserr.New(constant.ErrRuntimeStart).Error() + ":" + stderrBuf.String()) - } - return nil -} - -func SyncRuntimeContainerStatus(runtime *model.Runtime) error { - env, err := gotenv.Unmarshal(runtime.Env) - if err != nil { - return err - } - var containerNames []string - if containerName, ok := env["CONTAINER_NAME"]; !ok { - return buserr.New("ErrContainerNameNotFound") - } else { - containerNames = append(containerNames, containerName) - } - cli, err := docker.NewClient() - if err != nil { - return err - } - defer cli.Close() - containers, err := cli.ListContainersByName(containerNames) - if err != nil { - return err - } - if len(containers) == 0 { - return buserr.WithNameAndErr("ErrContainerNotFound", containerNames[0], nil) - } - container := containers[0] - - switch container.State { - case "exited": - runtime.Status = constant.RuntimeError - case "running": - runtime.Status = constant.RuntimeRunning - case "paused": - runtime.Status = constant.RuntimeStopped - default: - if runtime.Status != constant.RuntimeBuildIng { - runtime.Status = constant.RuntimeStopped - } - } - - return runtimeRepo.Save(runtime) -} - -func buildRuntime(runtime *model.Runtime, oldImageID string, rebuild bool) { - runtimePath := runtime.GetPath() - composePath := runtime.GetComposePath() - logPath := path.Join(runtimePath, "build.log") - - logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666) - if err != nil { - global.LOG.Errorf("failed to open log file: %v", err) - return - } - defer func() { - _ = logFile.Close() - }() - - cmd := exec.Command("docker-compose", "-f", composePath, "build") - multiWriterStdout := io.MultiWriter(os.Stdout, logFile) - cmd.Stdout = multiWriterStdout - var stderrBuf bytes.Buffer - multiWriterStderr := io.MultiWriter(&stderrBuf, logFile, os.Stderr) - cmd.Stderr = multiWriterStderr - - err = cmd.Run() - if err != nil { - runtime.Status = constant.RuntimeError - runtime.Message = buserr.New(constant.ErrImageBuildErr).Error() + ":" + stderrBuf.String() - } else { - runtime.Status = constant.RuntimeNormal - runtime.Message = "" - if oldImageID != "" { - client, err := docker.NewClient() - if err == nil { - defer client.Close() - newImageID, err := client.GetImageIDByName(runtime.Image) - if err == nil && newImageID != oldImageID { - global.LOG.Infof("delete imageID [%s] ", oldImageID) - if err := client.DeleteImage(oldImageID); err != nil { - global.LOG.Errorf("delete imageID [%s] error %v", oldImageID, err) - } else { - global.LOG.Infof("delete old image success") - } - } - } else { - global.LOG.Errorf("delete imageID [%s] error %v", oldImageID, err) - } - } - if rebuild && runtime.ID > 0 { - websites, _ := websiteRepo.GetBy(websiteRepo.WithRuntimeID(runtime.ID)) - if len(websites) > 0 { - installService := NewIAppInstalledService() - installMap := make(map[uint]string) - for _, website := range websites { - if website.AppInstallID > 0 { - installMap[website.AppInstallID] = website.PrimaryDomain - } - } - for installID, domain := range installMap { - go func(installID uint, domain string) { - global.LOG.Infof("rebuild php runtime [%s] domain [%s]", runtime.Name, domain) - if err := installService.Operate(request.AppInstalledOperate{ - InstallId: installID, - Operate: constant.Rebuild, - }); err != nil { - global.LOG.Errorf("rebuild php runtime [%s] domain [%s] error %v", runtime.Name, domain, err) - } - }(installID, domain) - } - } - } - } - _ = runtimeRepo.Save(runtime) -} - -func handleParams(create request.RuntimeCreate, projectDir string) (composeContent []byte, envContent []byte, forms []byte, err error) { - fileOp := files.NewFileOp() - composeContent, err = fileOp.GetContent(path.Join(projectDir, "docker-compose.yml")) - if err != nil { - return - } - envPath := path.Join(projectDir, ".env") - if !fileOp.Stat(envPath) { - _ = fileOp.CreateFile(envPath) - } - env, err := gotenv.Read(envPath) - if err != nil { - return - } - switch create.Type { - case constant.RuntimePHP: - create.Params["IMAGE_NAME"] = create.Image - forms, err = fileOp.GetContent(path.Join(projectDir, "config.json")) - if err != nil { - return - } - if extends, ok := create.Params["PHP_EXTENSIONS"]; ok { - if extendsArray, ok := extends.([]interface{}); ok { - strArray := make([]string, len(extendsArray)) - for i, v := range extendsArray { - strArray[i] = strings.ToLower(fmt.Sprintf("%v", v)) - } - create.Params["PHP_EXTENSIONS"] = strings.Join(strArray, ",") - } - } - create.Params["CONTAINER_PACKAGE_URL"] = create.Source - case constant.RuntimeNode: - create.Params["CODE_DIR"] = create.CodeDir - create.Params["NODE_VERSION"] = create.Version - create.Params["PANEL_APP_PORT_HTTP"] = create.Port - if create.NodeConfig.Install { - create.Params["RUN_INSTALL"] = "1" - } else { - create.Params["RUN_INSTALL"] = "0" - } - create.Params["CONTAINER_PACKAGE_URL"] = create.Source - - composeContent, err = handleCompose(env, composeContent, create, projectDir) - if err != nil { - return - } - case constant.RuntimeJava: - create.Params["CODE_DIR"] = create.CodeDir - create.Params["JAVA_VERSION"] = create.Version - create.Params["PANEL_APP_PORT_HTTP"] = create.Port - composeContent, err = handleCompose(env, composeContent, create, projectDir) - if err != nil { - return - } - case constant.RuntimeGo: - create.Params["CODE_DIR"] = create.CodeDir - create.Params["GO_VERSION"] = create.Version - create.Params["PANEL_APP_PORT_HTTP"] = create.Port - composeContent, err = handleCompose(env, composeContent, create, projectDir) - if err != nil { - return - } - } - - newMap := make(map[string]string) - handleMap(create.Params, newMap) - for k, v := range newMap { - env[k] = v - } - - envStr, err := gotenv.Marshal(env) - if err != nil { - return - } - if err = gotenv.Write(env, envPath); err != nil { - return - } - envContent = []byte(envStr) - return -} - -func handleCompose(env gotenv.Env, composeContent []byte, create request.RuntimeCreate, projectDir string) (composeByte []byte, err error) { - existMap := make(map[string]interface{}) - composeMap := make(map[string]interface{}) - if err = yaml.Unmarshal(composeContent, &composeMap); err != nil { - return - } - services, serviceValid := composeMap["services"].(map[string]interface{}) - if !serviceValid { - err = buserr.New(constant.ErrFileParse) - return - } - serviceName := "" - serviceValue := make(map[string]interface{}) - for name, service := range services { - serviceName = name - serviceValue = service.(map[string]interface{}) - _, ok := serviceValue["ports"].([]interface{}) - if ok { - var ports []interface{} - - switch create.Type { - case constant.RuntimeNode: - ports = append(ports, "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${NODE_APP_PORT}") - case constant.RuntimeJava: - ports = append(ports, "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${JAVA_APP_PORT}") - case constant.RuntimeGo: - ports = append(ports, "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${GO_APP_PORT}") - - } - - for i, port := range create.ExposedPorts { - containerPortStr := fmt.Sprintf("CONTAINER_PORT_%d", i) - hostPortStr := fmt.Sprintf("HOST_PORT_%d", i) - existMap[containerPortStr] = struct{}{} - existMap[hostPortStr] = struct{}{} - ports = append(ports, fmt.Sprintf("${HOST_IP}:${%s}:${%s}", hostPortStr, containerPortStr)) - create.Params[containerPortStr] = port.ContainerPort - create.Params[hostPortStr] = port.HostPort - } - serviceValue["ports"] = ports - } - break - } - for k := range env { - if strings.Contains(k, "CONTAINER_PORT_") || strings.Contains(k, "HOST_PORT_") { - if _, ok := existMap[k]; !ok { - delete(env, k) - } - } - } - - services[serviceName] = serviceValue - composeMap["services"] = services - composeByte, err = yaml.Marshal(composeMap) - if err != nil { - return - } - fileOp := files.NewFileOp() - _ = fileOp.SaveFile(path.Join(projectDir, "docker-compose.yml"), string(composeByte), 0644) - return -} - -func checkContainerName(name string) error { - dockerCli, err := docker.NewClient() - if err != nil { - return err - } - defer dockerCli.Close() - names, err := dockerCli.ListContainersByName([]string{name}) - if err != nil { - return err - } - if len(names) > 0 { - return buserr.New(constant.ErrContainerName) - } - return nil -} diff --git a/backend/app/service/setting.go b/backend/app/service/setting.go deleted file mode 100644 index 9cb8518be..000000000 --- a/backend/app/service/setting.go +++ /dev/null @@ -1,86 +0,0 @@ -package service - -import ( - "encoding/json" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/robfig/cron/v3" -) - -type SettingService struct{} - -type ISettingService interface { - GetSettingInfo() (*dto.SettingInfo, error) - Update(key, value string) error -} - -func NewISettingService() ISettingService { - return &SettingService{} -} - -func (u *SettingService) GetSettingInfo() (*dto.SettingInfo, error) { - setting, err := settingRepo.GetList() - if err != nil { - return nil, constant.ErrRecordNotFound - } - settingMap := make(map[string]string) - for _, set := range setting { - settingMap[set.Key] = set.Value - } - var info dto.SettingInfo - arr, err := json.Marshal(settingMap) - if err != nil { - return nil, err - } - if err := json.Unmarshal(arr, &info); err != nil { - return nil, err - } - - info.LocalTime = time.Now().Format("2006-01-02 15:04:05 MST -0700") - return &info, err -} - -func (u *SettingService) Update(key, value string) error { - switch key { - case "MonitorStatus": - if value == "enable" && global.MonitorCronID == 0 { - interval, err := settingRepo.Get(settingRepo.WithByKey("MonitorInterval")) - if err != nil { - return err - } - if err := StartMonitor(false, interval.Value); err != nil { - return err - } - } - if value == "disable" && global.MonitorCronID != 0 { - monitorCancel() - global.Cron.Remove(cron.EntryID(global.MonitorCronID)) - global.MonitorCronID = 0 - } - case "MonitorInterval": - status, err := settingRepo.Get(settingRepo.WithByKey("MonitorStatus")) - if err != nil { - return err - } - if status.Value == "enable" && global.MonitorCronID != 0 { - if err := StartMonitor(true, value); err != nil { - return err - } - } - case "AppStoreLastModified": - exist, _ := settingRepo.Get(settingRepo.WithByKey("AppStoreLastModified")) - if exist.ID == 0 { - _ = settingRepo.Create("AppStoreLastModified", value) - return nil - } - } - - if err := settingRepo.Update(key, value); err != nil { - return err - } - - return nil -} diff --git a/backend/app/service/snapshot.go b/backend/app/service/snapshot.go deleted file mode 100644 index 07f14bfdd..000000000 --- a/backend/app/service/snapshot.go +++ /dev/null @@ -1,556 +0,0 @@ -package service - -import ( - "context" - "encoding/json" - "fmt" - "os" - "path" - "strings" - "sync" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/compose" - "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/jinzhu/copier" - "github.com/pkg/errors" - "github.com/shirou/gopsutil/v3/host" -) - -type SnapshotService struct { - OriginalPath string -} - -type ISnapshotService interface { - SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error) - SnapshotCreate(req dto.SnapshotCreate) error - SnapshotRecover(req dto.SnapshotRecover) error - SnapshotRollback(req dto.SnapshotRecover) error - SnapshotImport(req dto.SnapshotImport) error - Delete(req dto.SnapshotBatchDelete) error - - LoadSnapShotStatus(id uint) (*dto.SnapshotStatus, error) - - UpdateDescription(req dto.UpdateDescription) error - readFromJson(path string) (SnapshotJson, error) - - HandleSnapshot(isCronjob bool, logPath string, req dto.SnapshotCreate, timeNow string, secret string) (string, error) -} - -func NewISnapshotService() ISnapshotService { - return &SnapshotService{} -} - -func (u *SnapshotService) SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error) { - total, systemBackups, err := snapshotRepo.Page(req.Page, req.PageSize, commonRepo.WithLikeName(req.Info)) - if err != nil { - return 0, nil, err - } - dtoSnap, err := loadSnapSize(systemBackups) - if err != nil { - return 0, nil, err - } - return total, dtoSnap, err -} - -func (u *SnapshotService) SnapshotImport(req dto.SnapshotImport) error { - if len(req.Names) == 0 { - return fmt.Errorf("incorrect snapshot request body: %v", req.Names) - } - for _, snapName := range req.Names { - snap, _ := snapshotRepo.Get(commonRepo.WithByName(strings.ReplaceAll(snapName, ".tar.gz", ""))) - if snap.ID != 0 { - return constant.ErrRecordExist - } - } - for _, snap := range req.Names { - shortName := strings.TrimPrefix(snap, "snapshot_") - nameItems := strings.Split(shortName, "_") - if !strings.HasPrefix(shortName, "1panel_v") || !strings.HasSuffix(shortName, ".tar.gz") || len(nameItems) < 3 { - return fmt.Errorf("incorrect snapshot name format of %s", shortName) - } - if strings.HasSuffix(snap, ".tar.gz") { - snap = strings.ReplaceAll(snap, ".tar.gz", "") - } - itemSnap := model.Snapshot{ - Name: snap, - From: req.From, - DefaultDownload: req.From, - Version: nameItems[1], - Description: req.Description, - Status: constant.StatusSuccess, - } - if err := snapshotRepo.Create(&itemSnap); err != nil { - return err - } - } - return nil -} - -func (u *SnapshotService) UpdateDescription(req dto.UpdateDescription) error { - return snapshotRepo.Update(req.ID, map[string]interface{}{"description": req.Description}) -} - -func (u *SnapshotService) LoadSnapShotStatus(id uint) (*dto.SnapshotStatus, error) { - var data dto.SnapshotStatus - status, err := snapshotRepo.GetStatus(id) - if err != nil { - return nil, err - } - if err := copier.Copy(&data, &status); err != nil { - return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - return &data, nil -} - -type SnapshotJson struct { - OldBaseDir string `json:"oldBaseDir"` - OldDockerDataDir string `json:"oldDockerDataDir"` - OldBackupDataDir string `json:"oldBackupDataDir"` - OldPanelDataDir string `json:"oldPanelDataDir"` - - BaseDir string `json:"baseDir"` - DockerDataDir string `json:"dockerDataDir"` - BackupDataDir string `json:"backupDataDir"` - PanelDataDir string `json:"panelDataDir"` - LiveRestoreEnabled bool `json:"liveRestoreEnabled"` -} - -func (u *SnapshotService) SnapshotCreate(req dto.SnapshotCreate) error { - if _, err := u.HandleSnapshot(false, "", req, time.Now().Format(constant.DateTimeSlimLayout), req.Secret); err != nil { - return err - } - return nil -} - -func (u *SnapshotService) SnapshotRecover(req dto.SnapshotRecover) error { - global.LOG.Info("start to recover panel by snapshot now") - snap, err := snapshotRepo.Get(commonRepo.WithByID(req.ID)) - if err != nil { - return err - } - if hasOs(snap.Name) && !strings.Contains(snap.Name, loadOs()) { - return fmt.Errorf("restoring snapshots(%s) between different server architectures(%s) is not supported", snap.Name, loadOs()) - } - if !req.IsNew && len(snap.InterruptStep) != 0 && len(snap.RollbackStatus) != 0 { - return fmt.Errorf("the snapshot has been rolled back and cannot be restored again") - } - - baseDir := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("system/%s", snap.Name)) - if _, err := os.Stat(baseDir); err != nil && os.IsNotExist(err) { - _ = os.MkdirAll(baseDir, os.ModePerm) - } - - _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"recover_status": constant.StatusWaiting}) - _ = settingRepo.Update("SystemStatus", "Recovering") - go u.HandleSnapshotRecover(snap, true, req) - return nil -} - -func (u *SnapshotService) SnapshotRollback(req dto.SnapshotRecover) error { - global.LOG.Info("start to rollback now") - snap, err := snapshotRepo.Get(commonRepo.WithByID(req.ID)) - if err != nil { - return err - } - req.IsNew = false - snap.InterruptStep = "Readjson" - go u.HandleSnapshotRecover(snap, false, req) - return nil -} - -func (u *SnapshotService) readFromJson(path string) (SnapshotJson, error) { - var snap SnapshotJson - if _, err := os.Stat(path); err != nil { - return snap, fmt.Errorf("find snapshot json file in recover package failed, err: %v", err) - } - fileByte, err := os.ReadFile(path) - if err != nil { - return snap, fmt.Errorf("read file from path %s failed, err: %v", path, err) - } - if err := json.Unmarshal(fileByte, &snap); err != nil { - return snap, fmt.Errorf("unmarshal snapjson failed, err: %v", err) - } - return snap, nil -} - -func (u *SnapshotService) HandleSnapshot(isCronjob bool, logPath string, req dto.SnapshotCreate, timeNow string, secret string) (string, error) { - localDir, err := loadLocalDir() - if err != nil { - return "", err - } - var ( - rootDir string - snap model.Snapshot - snapStatus model.SnapshotStatus - ) - - if req.ID == 0 { - versionItem, _ := settingRepo.Get(settingRepo.WithByKey("SystemVersion")) - - name := fmt.Sprintf("1panel_%s_%s_%s", versionItem.Value, loadOs(), timeNow) - if isCronjob { - name = fmt.Sprintf("snapshot_1panel_%s_%s_%s", versionItem.Value, loadOs(), timeNow) - } - rootDir = path.Join(localDir, "system", name) - - snap = model.Snapshot{ - Name: name, - Description: req.Description, - From: req.From, - DefaultDownload: req.DefaultDownload, - Version: versionItem.Value, - Status: constant.StatusWaiting, - } - _ = snapshotRepo.Create(&snap) - snapStatus.SnapID = snap.ID - _ = snapshotRepo.CreateStatus(&snapStatus) - } else { - snap, err = snapshotRepo.Get(commonRepo.WithByID(req.ID)) - if err != nil { - return "", err - } - _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusWaiting}) - snapStatus, _ = snapshotRepo.GetStatus(snap.ID) - if snapStatus.ID == 0 { - snapStatus.SnapID = snap.ID - _ = snapshotRepo.CreateStatus(&snapStatus) - } - rootDir = path.Join(localDir, fmt.Sprintf("system/%s", snap.Name)) - } - - var wg sync.WaitGroup - itemHelper := snapHelper{SnapID: snap.ID, Status: &snapStatus, Wg: &wg, FileOp: files.NewFileOp(), Ctx: context.Background()} - backupPanelDir := path.Join(rootDir, "1panel") - _ = os.MkdirAll(backupPanelDir, os.ModePerm) - backupDockerDir := path.Join(rootDir, "docker") - _ = os.MkdirAll(backupDockerDir, os.ModePerm) - - jsonItem := SnapshotJson{ - BaseDir: global.CONF.System.BaseDir, - BackupDataDir: localDir, - PanelDataDir: path.Join(global.CONF.System.BaseDir, "1panel"), - } - loadLogByStatus(snapStatus, logPath) - if snapStatus.PanelInfo != constant.StatusDone { - wg.Add(1) - go snapJson(itemHelper, jsonItem, rootDir) - } - if snapStatus.Panel != constant.StatusDone { - wg.Add(1) - go snapPanel(itemHelper, backupPanelDir) - } - if snapStatus.DaemonJson != constant.StatusDone { - wg.Add(1) - go snapDaemonJson(itemHelper, backupDockerDir) - } - if snapStatus.AppData != constant.StatusDone { - wg.Add(1) - go snapAppData(itemHelper, backupDockerDir) - } - if snapStatus.BackupData != constant.StatusDone { - wg.Add(1) - go snapBackup(itemHelper, localDir, backupPanelDir) - } - - if !isCronjob { - go func() { - wg.Wait() - if !checkIsAllDone(snap.ID) { - _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed}) - return - } - if snapStatus.PanelData != constant.StatusDone { - snapPanelData(itemHelper, localDir, backupPanelDir) - } - if snapStatus.PanelData != constant.StatusDone { - _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed}) - return - } - if snapStatus.Compress != constant.StatusDone { - snapCompress(itemHelper, rootDir, secret) - } - if snapStatus.Compress != constant.StatusDone { - _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed}) - return - } - if snapStatus.Upload != constant.StatusDone { - snapUpload(itemHelper, req.From, fmt.Sprintf("%s.tar.gz", rootDir)) - } - if snapStatus.Upload != constant.StatusDone { - _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed}) - return - } - _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusSuccess}) - }() - return "", nil - } - wg.Wait() - if !checkIsAllDone(snap.ID) { - _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed}) - loadLogByStatus(snapStatus, logPath) - return snap.Name, fmt.Errorf("snapshot %s backup failed", snap.Name) - } - loadLogByStatus(snapStatus, logPath) - snapPanelData(itemHelper, localDir, backupPanelDir) - if snapStatus.PanelData != constant.StatusDone { - _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed}) - loadLogByStatus(snapStatus, logPath) - return snap.Name, fmt.Errorf("snapshot %s 1panel data failed", snap.Name) - } - loadLogByStatus(snapStatus, logPath) - snapCompress(itemHelper, rootDir, secret) - if snapStatus.Compress != constant.StatusDone { - _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed}) - loadLogByStatus(snapStatus, logPath) - return snap.Name, fmt.Errorf("snapshot %s compress failed", snap.Name) - } - loadLogByStatus(snapStatus, logPath) - snapUpload(itemHelper, req.From, fmt.Sprintf("%s.tar.gz", rootDir)) - if snapStatus.Upload != constant.StatusDone { - _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed}) - loadLogByStatus(snapStatus, logPath) - return snap.Name, fmt.Errorf("snapshot %s upload failed", snap.Name) - } - _ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusSuccess}) - loadLogByStatus(snapStatus, logPath) - return snap.Name, nil -} - -func (u *SnapshotService) Delete(req dto.SnapshotBatchDelete) error { - snaps, _ := snapshotRepo.GetList(commonRepo.WithIdsIn(req.Ids)) - for _, snap := range snaps { - if req.DeleteWithFile { - targetAccounts, err := loadClientMap(snap.From) - if err != nil { - return err - } - for _, item := range targetAccounts { - global.LOG.Debugf("remove snapshot file %s.tar.gz from %s", snap.Name, item.backType) - _, _ = item.client.Delete(path.Join(item.backupPath, "system_snapshot", snap.Name+".tar.gz")) - } - } - - _ = snapshotRepo.DeleteStatus(snap.ID) - if err := snapshotRepo.Delete(commonRepo.WithByID(snap.ID)); err != nil { - return err - } - } - return nil -} - -func updateRecoverStatus(id uint, isRecover bool, interruptStep, status, message string) { - if isRecover { - if status != constant.StatusSuccess { - global.LOG.Errorf("recover failed, err: %s", message) - } - if err := snapshotRepo.Update(id, map[string]interface{}{ - "interrupt_step": interruptStep, - "recover_status": status, - "recover_message": message, - "last_recovered_at": time.Now().Format(constant.DateTimeLayout), - }); err != nil { - global.LOG.Errorf("update snap recover status failed, err: %v", err) - } - _ = settingRepo.Update("SystemStatus", "Free") - return - } - _ = settingRepo.Update("SystemStatus", "Free") - if status == constant.StatusSuccess { - if err := snapshotRepo.Update(id, map[string]interface{}{ - "recover_status": "", - "recover_message": "", - "interrupt_step": "", - "rollback_status": "", - "rollback_message": "", - "last_rollbacked_at": time.Now().Format(constant.DateTimeLayout), - }); err != nil { - global.LOG.Errorf("update snap recover status failed, err: %v", err) - } - return - } - global.LOG.Errorf("rollback failed, err: %s", message) - if err := snapshotRepo.Update(id, map[string]interface{}{ - "rollback_status": status, - "rollback_message": message, - "last_rollbacked_at": time.Now().Format(constant.DateTimeLayout), - }); err != nil { - global.LOG.Errorf("update snap recover status failed, err: %v", err) - } -} - -func (u *SnapshotService) handleUnTar(sourceDir, targetDir string, secret string) error { - if _, err := os.Stat(targetDir); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(targetDir, os.ModePerm); err != nil { - return err - } - } - commands := "" - if len(secret) != 0 { - extraCmd := "openssl enc -d -aes-256-cbc -k '" + secret + "' -in " + sourceDir + " | " - commands = fmt.Sprintf("%s tar -zxvf - -C %s", extraCmd, targetDir+" > /dev/null 2>&1") - global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******")) - } else { - commands = fmt.Sprintf("tar zxvfC %s %s", sourceDir, targetDir) - global.LOG.Debug(commands) - } - stdout, err := cmd.ExecWithTimeOut(commands, 30*time.Minute) - if err != nil { - if len(stdout) != 0 { - global.LOG.Errorf("do handle untar failed, stdout: %s, err: %v", stdout, err) - return fmt.Errorf("do handle untar failed, stdout: %s, err: %v", stdout, err) - } - } - return nil -} - -func rebuildAllAppInstall() error { - global.LOG.Debug("start to rebuild all app") - appInstalls, err := appInstallRepo.ListBy() - if err != nil { - global.LOG.Errorf("get all app installed for rebuild failed, err: %v", err) - return err - } - var wg sync.WaitGroup - for i := 0; i < len(appInstalls); i++ { - wg.Add(1) - appInstalls[i].Status = constant.Rebuilding - _ = appInstallRepo.Save(context.Background(), &appInstalls[i]) - go func(app model.AppInstall) { - defer wg.Done() - dockerComposePath := app.GetComposePath() - out, err := compose.Down(dockerComposePath) - if err != nil { - _ = handleErr(app, err, out) - return - } - out, err = compose.Up(dockerComposePath) - if err != nil { - _ = handleErr(app, err, out) - return - } - app.Status = constant.Running - _ = appInstallRepo.Save(context.Background(), &app) - }(appInstalls[i]) - } - wg.Wait() - return nil -} - -func checkIsAllDone(snapID uint) bool { - status, err := snapshotRepo.GetStatus(snapID) - if err != nil { - return false - } - isOK, _ := checkAllDone(status) - return isOK -} - -func checkAllDone(status model.SnapshotStatus) (bool, string) { - if status.Panel != constant.StatusDone { - return false, status.Panel - } - if status.PanelInfo != constant.StatusDone { - return false, status.PanelInfo - } - if status.DaemonJson != constant.StatusDone { - return false, status.DaemonJson - } - if status.AppData != constant.StatusDone { - return false, status.AppData - } - if status.BackupData != constant.StatusDone { - return false, status.BackupData - } - return true, "" -} - -func loadLogByStatus(status model.SnapshotStatus, logPath string) { - logs := "" - logs += fmt.Sprintf("Write 1Panel basic information: %s \n", status.PanelInfo) - logs += fmt.Sprintf("Backup 1Panel system files: %s \n", status.Panel) - logs += fmt.Sprintf("Backup Docker configuration file: %s \n", status.DaemonJson) - logs += fmt.Sprintf("Backup installed apps from 1Panel: %s \n", status.AppData) - logs += fmt.Sprintf("Backup 1Panel data directory: %s \n", status.PanelData) - logs += fmt.Sprintf("Backup local backup directory for 1Panel: %s \n", status.BackupData) - logs += fmt.Sprintf("Create snapshot file: %s \n", status.Compress) - logs += fmt.Sprintf("Snapshot size: %s \n", status.Size) - logs += fmt.Sprintf("Upload snapshot file: %s \n", status.Upload) - - file, err := os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) - if err != nil { - return - } - defer file.Close() - _, _ = file.Write([]byte(logs)) -} - -func hasOs(name string) bool { - return strings.Contains(name, "amd64") || - strings.Contains(name, "arm64") || - strings.Contains(name, "armv7") || - strings.Contains(name, "ppc64le") || - strings.Contains(name, "s390x") -} - -func loadOs() string { - hostInfo, _ := host.Info() - switch hostInfo.KernelArch { - case "x86_64": - return "amd64" - case "armv7l": - return "armv7" - default: - return hostInfo.KernelArch - } -} - -func loadSnapSize(records []model.Snapshot) ([]dto.SnapshotInfo, error) { - var datas []dto.SnapshotInfo - clientMap := make(map[string]loadSizeHelper) - var wg sync.WaitGroup - for i := 0; i < len(records); i++ { - var item dto.SnapshotInfo - if err := copier.Copy(&item, &records[i]); err != nil { - return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - itemPath := fmt.Sprintf("system_snapshot/%s.tar.gz", item.Name) - if _, ok := clientMap[records[i].DefaultDownload]; !ok { - backup, err := backupRepo.Get(commonRepo.WithByType(records[i].DefaultDownload)) - if err != nil { - global.LOG.Errorf("load backup model %s from db failed, err: %v", records[i].DefaultDownload, err) - clientMap[records[i].DefaultDownload] = loadSizeHelper{} - datas = append(datas, item) - continue - } - client, err := NewIBackupService().NewClient(&backup) - if err != nil { - global.LOG.Errorf("load backup client %s from db failed, err: %v", records[i].DefaultDownload, err) - clientMap[records[i].DefaultDownload] = loadSizeHelper{} - datas = append(datas, item) - continue - } - item.Size, _ = client.Size(path.Join(strings.TrimLeft(backup.BackupPath, "/"), itemPath)) - datas = append(datas, item) - clientMap[records[i].DefaultDownload] = loadSizeHelper{backupPath: strings.TrimLeft(backup.BackupPath, "/"), client: client, isOk: true} - continue - } - if clientMap[records[i].DefaultDownload].isOk { - wg.Add(1) - go func(index int) { - item.Size, _ = clientMap[records[index].DefaultDownload].client.Size(path.Join(clientMap[records[index].DefaultDownload].backupPath, itemPath)) - datas = append(datas, item) - wg.Done() - }(i) - } else { - datas = append(datas, item) - } - } - wg.Wait() - return datas, nil -} diff --git a/backend/app/service/snapshot_create.go b/backend/app/service/snapshot_create.go deleted file mode 100644 index 9649326c0..000000000 --- a/backend/app/service/snapshot_create.go +++ /dev/null @@ -1,280 +0,0 @@ -package service - -import ( - "context" - "encoding/json" - "fmt" - "os" - "path" - "regexp" - "strings" - "sync" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/1Panel-dev/1Panel/backend/utils/files" -) - -type snapHelper struct { - SnapID uint - Status *model.SnapshotStatus - Ctx context.Context - FileOp files.FileOp - Wg *sync.WaitGroup -} - -func snapJson(snap snapHelper, snapJson SnapshotJson, targetDir string) { - defer snap.Wg.Done() - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel_info": constant.Running}) - status := constant.StatusDone - remarkInfo, _ := json.MarshalIndent(snapJson, "", "\t") - if err := os.WriteFile(fmt.Sprintf("%s/snapshot.json", targetDir), remarkInfo, 0640); err != nil { - status = err.Error() - } - snap.Status.PanelInfo = status - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel_info": status}) -} - -func snapPanel(snap snapHelper, targetDir string) { - defer snap.Wg.Done() - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": constant.Running}) - status := constant.StatusDone - if err := common.CopyFile("/usr/local/bin/1panel", path.Join(targetDir, "1panel")); err != nil { - status = err.Error() - } - - if err := common.CopyFile("/usr/local/bin/1pctl", targetDir); err != nil { - status = err.Error() - } - - if err := common.CopyFile("/etc/systemd/system/1panel.service", targetDir); err != nil { - status = err.Error() - } - snap.Status.Panel = status - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": status}) -} - -func snapDaemonJson(snap snapHelper, targetDir string) { - defer snap.Wg.Done() - status := constant.StatusDone - if !snap.FileOp.Stat("/etc/docker/daemon.json") { - snap.Status.DaemonJson = status - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"daemon_json": status}) - return - } - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"daemon_json": constant.Running}) - if err := common.CopyFile("/etc/docker/daemon.json", targetDir); err != nil { - status = err.Error() - } - snap.Status.DaemonJson = status - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"daemon_json": status}) -} - -func snapAppData(snap snapHelper, targetDir string) { - defer snap.Wg.Done() - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": constant.Running}) - appInstalls, err := appInstallRepo.ListBy() - if err != nil { - snap.Status.AppData = err.Error() - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": err.Error()}) - return - } - runtimes, err := runtimeRepo.List() - if err != nil { - snap.Status.AppData = err.Error() - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": err.Error()}) - return - } - imageRegex := regexp.MustCompile(`image:\s*(.*)`) - var imageSaveList []string - existStr, _ := cmd.Exec("docker images | awk '{print $1\":\"$2}' | grep -v REPOSITORY:TAG") - existImages := strings.Split(existStr, "\n") - duplicateMap := make(map[string]bool) - for _, app := range appInstalls { - matches := imageRegex.FindAllStringSubmatch(app.DockerCompose, -1) - for _, match := range matches { - for _, existImage := range existImages { - if match[1] == existImage && !duplicateMap[match[1]] { - imageSaveList = append(imageSaveList, match[1]) - duplicateMap[match[1]] = true - } - } - } - } - for _, runtime := range runtimes { - for _, existImage := range existImages { - if runtime.Image == existImage && !duplicateMap[runtime.Image] { - imageSaveList = append(imageSaveList, runtime.Image) - duplicateMap[runtime.Image] = true - } - } - } - - if len(imageSaveList) != 0 { - global.LOG.Debugf("docker save %s | gzip -c > %s", strings.Join(imageSaveList, " "), path.Join(targetDir, "docker_image.tar")) - std, err := cmd.Execf("docker save %s | gzip -c > %s", strings.Join(imageSaveList, " "), path.Join(targetDir, "docker_image.tar")) - if err != nil { - snap.Status.AppData = err.Error() - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": std}) - return - } - } - snap.Status.AppData = constant.StatusDone - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"app_data": constant.StatusDone}) -} - -func snapBackup(snap snapHelper, localDir, targetDir string) { - defer snap.Wg.Done() - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"backup_data": constant.Running}) - status := constant.StatusDone - if err := handleSnapTar(localDir, targetDir, "1panel_backup.tar.gz", "./system;./system_snapshot;", ""); err != nil { - status = err.Error() - } - snap.Status.BackupData = status - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"backup_data": status}) -} - -func snapPanelData(snap snapHelper, localDir, targetDir string) { - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel_data": constant.Running}) - status := constant.StatusDone - dataDir := path.Join(global.CONF.System.BaseDir, "1panel") - exclusionRules := "./tmp;./log;./cache;./db/1Panel.db-*;" - if strings.Contains(localDir, dataDir) { - exclusionRules += ("." + strings.ReplaceAll(localDir, dataDir, "") + ";") - } - ignoreVal, _ := settingRepo.Get(settingRepo.WithByKey("SnapshotIgnore")) - rules := strings.Split(ignoreVal.Value, ",") - for _, ignore := range rules { - if len(ignore) == 0 || cmd.CheckIllegal(ignore) { - continue - } - exclusionRules += ("." + strings.ReplaceAll(ignore, dataDir, "") + ";") - } - _ = snapshotRepo.Update(snap.SnapID, map[string]interface{}{"status": "OnSaveData"}) - sysIP, _ := settingRepo.Get(settingRepo.WithByKey("SystemIP")) - _ = settingRepo.Update("SystemIP", "") - checkPointOfWal() - if err := handleSnapTar(dataDir, targetDir, "1panel_data.tar.gz", exclusionRules, ""); err != nil { - status = err.Error() - } - _ = snapshotRepo.Update(snap.SnapID, map[string]interface{}{"status": constant.StatusWaiting}) - - snap.Status.PanelData = status - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel_data": status}) - _ = settingRepo.Update("SystemIP", sysIP.Value) -} - -func snapCompress(snap snapHelper, rootDir string, secret string) { - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"compress": constant.StatusRunning}) - tmpDir := path.Join(global.CONF.System.TmpDir, "system") - fileName := fmt.Sprintf("%s.tar.gz", path.Base(rootDir)) - if err := handleSnapTar(rootDir, tmpDir, fileName, "", secret); err != nil { - snap.Status.Compress = err.Error() - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"compress": err.Error()}) - return - } - - stat, err := os.Stat(path.Join(tmpDir, fileName)) - if err != nil { - snap.Status.Compress = err.Error() - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"compress": err.Error()}) - return - } - size := common.LoadSizeUnit2F(float64(stat.Size())) - global.LOG.Debugf("compress successful! size of file: %s", size) - snap.Status.Compress = constant.StatusDone - snap.Status.Size = size - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"compress": constant.StatusDone, "size": size}) - - global.LOG.Debugf("remove snapshot file %s", rootDir) - _ = os.RemoveAll(rootDir) -} - -func snapUpload(snap snapHelper, accounts string, file string) { - source := path.Join(global.CONF.System.TmpDir, "system", path.Base(file)) - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"upload": constant.StatusUploading}) - accountMap, err := loadClientMap(accounts) - if err != nil { - snap.Status.Upload = err.Error() - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"upload": err.Error()}) - return - } - targetAccounts := strings.Split(accounts, ",") - for _, item := range targetAccounts { - global.LOG.Debugf("start upload snapshot to %s, path: %s", item, path.Join(accountMap[item].backupPath, "system_snapshot", path.Base(file))) - if _, err := accountMap[item].client.Upload(source, path.Join(accountMap[item].backupPath, "system_snapshot", path.Base(file))); err != nil { - global.LOG.Debugf("upload to %s failed, err: %v", item, err) - snap.Status.Upload = err.Error() - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"upload": err.Error()}) - return - } - global.LOG.Debugf("upload to %s successful", item) - } - snap.Status.Upload = constant.StatusDone - _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"upload": constant.StatusDone}) - - global.LOG.Debugf("remove snapshot file %s", source) - _ = os.Remove(source) -} - -func handleSnapTar(sourceDir, targetDir, name, exclusionRules string, secret string) error { - if _, err := os.Stat(targetDir); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(targetDir, os.ModePerm); err != nil { - return err - } - } - - exMap := make(map[string]struct{}) - exStr := "" - excludes := strings.Split(exclusionRules, ";") - excludes = append(excludes, "*.sock") - for _, exclude := range excludes { - if len(exclude) == 0 { - continue - } - if _, ok := exMap[exclude]; ok { - continue - } - exStr += " --exclude " - exStr += exclude - exMap[exclude] = struct{}{} - } - path := "" - if strings.Contains(sourceDir, "/") { - itemDir := strings.ReplaceAll(sourceDir[strings.LastIndex(sourceDir, "/"):], "/", "") - aheadDir := sourceDir[:strings.LastIndex(sourceDir, "/")] - if len(aheadDir) == 0 { - aheadDir = "/" - } - path += fmt.Sprintf("-C %s %s", aheadDir, itemDir) - } else { - path = sourceDir - } - commands := "" - if len(secret) != 0 { - extraCmd := "| openssl enc -aes-256-cbc -salt -k '" + secret + "' -out" - commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read -zcf %s %s %s %s", " -"+exStr, path, extraCmd, targetDir+"/"+name) - global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******")) - } else { - commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read -zcf %s %s -C %s .", targetDir+"/"+name, exStr, sourceDir) - global.LOG.Debug(commands) - } - stdout, err := cmd.ExecWithTimeOut(commands, 30*time.Minute) - if err != nil { - if len(stdout) != 0 { - global.LOG.Errorf("do handle tar failed, stdout: %s, err: %v", stdout, err) - return fmt.Errorf("do handle tar failed, stdout: %s, err: %v", stdout, err) - } - } - return nil -} - -func checkPointOfWal() { - if err := global.DB.Exec("PRAGMA wal_checkpoint(TRUNCATE);").Error; err != nil { - global.LOG.Errorf("handle check point failed, err: %v", err) - } -} diff --git a/backend/app/service/snapshot_recover.go b/backend/app/service/snapshot_recover.go deleted file mode 100644 index d559c0843..000000000 --- a/backend/app/service/snapshot_recover.go +++ /dev/null @@ -1,265 +0,0 @@ -package service - -import ( - "context" - "fmt" - "os" - "path" - "strings" - "sync" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/pkg/errors" -) - -func (u *SnapshotService) HandleSnapshotRecover(snap model.Snapshot, isRecover bool, req dto.SnapshotRecover) { - _ = global.Cron.Stop() - defer func() { - global.Cron.Start() - }() - - snapFileDir := "" - if isRecover { - baseDir := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("system/%s", snap.Name)) - if _, err := os.Stat(baseDir); err != nil && os.IsNotExist(err) { - _ = os.MkdirAll(baseDir, os.ModePerm) - } - if req.IsNew || snap.InterruptStep == "Download" || req.ReDownload { - if err := handleDownloadSnapshot(snap, baseDir); err != nil { - updateRecoverStatus(snap.ID, isRecover, "Backup", constant.StatusFailed, err.Error()) - return - } - global.LOG.Debugf("download snapshot file to %s successful!", baseDir) - req.IsNew = true - } - if req.IsNew || snap.InterruptStep == "Decompress" { - if err := handleUnTar(fmt.Sprintf("%s/%s.tar.gz", baseDir, snap.Name), baseDir, req.Secret); err != nil { - updateRecoverStatus(snap.ID, isRecover, "Decompress", constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err)) - return - } - global.LOG.Debug("decompress snapshot file successful!", baseDir) - req.IsNew = true - } - if req.IsNew || snap.InterruptStep == "Backup" { - if err := backupBeforeRecover(snap, ""); err != nil { - updateRecoverStatus(snap.ID, isRecover, "Backup", constant.StatusFailed, fmt.Sprintf("handle backup before recover failed, err: %v", err)) - return - } - global.LOG.Debug("handle backup before recover successful!") - req.IsNew = true - } - snapFileDir = fmt.Sprintf("%s/%s", baseDir, snap.Name) - if _, err := os.Stat(snapFileDir); err != nil { - snapFileDir = baseDir - } - } else { - snapFileDir = fmt.Sprintf("%s/1panel_original/original_%s", global.CONF.System.BaseDir, snap.Name) - if _, err := os.Stat(snapFileDir); err != nil { - updateRecoverStatus(snap.ID, isRecover, "", constant.StatusFailed, fmt.Sprintf("cannot find the backup file %s, please try to recover again.", snapFileDir)) - return - } - } - snapJson, err := u.readFromJson(fmt.Sprintf("%s/snapshot.json", snapFileDir)) - if err != nil { - updateRecoverStatus(snap.ID, isRecover, "Readjson", constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err)) - return - } - if snap.InterruptStep == "Readjson" { - req.IsNew = true - } - if isRecover && (req.IsNew || snap.InterruptStep == "AppData") { - if err := recoverAppData(snapFileDir); err != nil { - updateRecoverStatus(snap.ID, isRecover, "DockerDir", constant.StatusFailed, fmt.Sprintf("handle recover app data failed, err: %v", err)) - return - } - global.LOG.Debug("recover app data from snapshot file successful!") - req.IsNew = true - } - if req.IsNew || snap.InterruptStep == "DaemonJson" { - fileOp := files.NewFileOp() - if err := recoverDaemonJson(snapFileDir, fileOp); err != nil { - updateRecoverStatus(snap.ID, isRecover, "DaemonJson", constant.StatusFailed, err.Error()) - return - } - global.LOG.Debug("recover daemon.json from snapshot file successful!") - req.IsNew = true - } - - if req.IsNew || snap.InterruptStep == "1PanelBinary" { - if err := recoverPanel(path.Join(snapFileDir, "1panel/1panel"), "/usr/local/bin"); err != nil { - updateRecoverStatus(snap.ID, isRecover, "1PanelBinary", constant.StatusFailed, err.Error()) - return - } - global.LOG.Debug("recover 1panel binary from snapshot file successful!") - req.IsNew = true - } - if req.IsNew || snap.InterruptStep == "1PctlBinary" { - if err := recoverPanel(path.Join(snapFileDir, "1panel/1pctl"), "/usr/local/bin"); err != nil { - updateRecoverStatus(snap.ID, isRecover, "1PctlBinary", constant.StatusFailed, err.Error()) - return - } - global.LOG.Debug("recover 1pctl from snapshot file successful!") - req.IsNew = true - } - if req.IsNew || snap.InterruptStep == "1PanelService" { - if err := recoverPanel(path.Join(snapFileDir, "1panel/1panel.service"), "/etc/systemd/system"); err != nil { - updateRecoverStatus(snap.ID, isRecover, "1PanelService", constant.StatusFailed, err.Error()) - return - } - global.LOG.Debug("recover 1panel service from snapshot file successful!") - req.IsNew = true - } - - if req.IsNew || snap.InterruptStep == "1PanelBackups" { - if err := u.handleUnTar(path.Join(snapFileDir, "/1panel/1panel_backup.tar.gz"), snapJson.BackupDataDir, ""); err != nil { - updateRecoverStatus(snap.ID, isRecover, "1PanelBackups", constant.StatusFailed, err.Error()) - return - } - global.LOG.Debug("recover 1panel backups from snapshot file successful!") - req.IsNew = true - } - - if req.IsNew || snap.InterruptStep == "1PanelData" { - checkPointOfWal() - if err := u.handleUnTar(path.Join(snapFileDir, "/1panel/1panel_data.tar.gz"), path.Join(snapJson.BaseDir, "1panel"), ""); err != nil { - updateRecoverStatus(snap.ID, isRecover, "1PanelData", constant.StatusFailed, err.Error()) - return - } - global.LOG.Debug("recover 1panel data from snapshot file successful!") - req.IsNew = true - } - _ = rebuildAllAppInstall() - restartCompose(path.Join(snapJson.BaseDir, "1panel/docker/compose")) - - global.LOG.Info("recover successful") - if !isRecover { - oriPath := fmt.Sprintf("%s/1panel_original/original_%s", global.CONF.System.BaseDir, snap.Name) - global.LOG.Debugf("remove the file %s after the operation is successful", oriPath) - _ = os.RemoveAll(oriPath) - } else { - global.LOG.Debugf("remove the file %s after the operation is successful", path.Dir(snapFileDir)) - _ = os.RemoveAll(path.Dir(snapFileDir)) - } - _, _ = cmd.Exec("systemctl daemon-reload && systemctl restart 1panel.service") -} - -func backupBeforeRecover(snap model.Snapshot, secret string) error { - baseDir := fmt.Sprintf("%s/1panel_original/original_%s", global.CONF.System.BaseDir, snap.Name) - var wg sync.WaitGroup - var status model.SnapshotStatus - itemHelper := snapHelper{SnapID: 0, Status: &status, Wg: &wg, FileOp: files.NewFileOp(), Ctx: context.Background()} - - jsonItem := SnapshotJson{ - BaseDir: global.CONF.System.BaseDir, - BackupDataDir: global.CONF.System.Backup, - PanelDataDir: path.Join(global.CONF.System.BaseDir, "1panel"), - } - _ = os.MkdirAll(path.Join(baseDir, "1panel"), os.ModePerm) - _ = os.MkdirAll(path.Join(baseDir, "docker"), os.ModePerm) - - wg.Add(4) - itemHelper.Wg = &wg - go snapJson(itemHelper, jsonItem, baseDir) - go snapPanel(itemHelper, path.Join(baseDir, "1panel")) - go snapDaemonJson(itemHelper, path.Join(baseDir, "docker")) - go snapBackup(itemHelper, global.CONF.System.Backup, path.Join(baseDir, "1panel")) - wg.Wait() - itemHelper.Status.AppData = constant.StatusDone - - allDone, msg := checkAllDone(status) - if !allDone { - return errors.New(msg) - } - snapPanelData(itemHelper, global.CONF.System.BaseDir, path.Join(baseDir, "1panel")) - if status.PanelData != constant.StatusDone { - return errors.New(status.PanelData) - } - return nil -} - -func handleDownloadSnapshot(snap model.Snapshot, targetDir string) error { - backup, err := backupRepo.Get(commonRepo.WithByType(snap.DefaultDownload)) - if err != nil { - return err - } - client, err := NewIBackupService().NewClient(&backup) - if err != nil { - return err - } - pathItem := backup.BackupPath - if backup.BackupPath != "/" { - pathItem = strings.TrimPrefix(backup.BackupPath, "/") - } - filePath := fmt.Sprintf("%s/%s.tar.gz", targetDir, snap.Name) - _ = os.RemoveAll(filePath) - ok, err := client.Download(path.Join(pathItem, fmt.Sprintf("system_snapshot/%s.tar.gz", snap.Name)), filePath) - if err != nil || !ok { - return fmt.Errorf("download file %s from %s failed, err: %v", snap.Name, backup.Type, err) - } - return nil -} - -func recoverAppData(src string) error { - if _, err := os.Stat(path.Join(src, "docker/docker_image.tar")); err != nil { - global.LOG.Debug("no such docker images in snapshot") - return nil - } - std, err := cmd.Execf("docker load < %s", path.Join(src, "docker/docker_image.tar")) - if err != nil { - return errors.New(std) - } - return err -} - -func recoverDaemonJson(src string, fileOp files.FileOp) error { - daemonJsonPath := "/etc/docker/daemon.json" - _, errSrc := os.Stat(path.Join(src, "docker/daemon.json")) - _, errPath := os.Stat(daemonJsonPath) - if os.IsNotExist(errSrc) && os.IsNotExist(errPath) { - global.LOG.Debug("the daemon.json file does not exist, nothing happens.") - return nil - } - if errSrc == nil { - if err := fileOp.CopyFile(path.Join(src, "docker/daemon.json"), "/etc/docker"); err != nil { - return fmt.Errorf("recover docker daemon.json failed, err: %v", err) - } - } - - _, _ = cmd.Exec("systemctl restart docker") - return nil -} - -func recoverPanel(src string, dst string) error { - if _, err := os.Stat(src); err != nil { - return fmt.Errorf("file is not found in %s, err: %v", src, err) - } - if err := common.CopyFile(src, dst); err != nil { - return fmt.Errorf("cp file failed, err: %v", err) - } - return nil -} - -func restartCompose(composePath string) { - composes, err := composeRepo.ListRecord() - if err != nil { - return - } - for _, compose := range composes { - pathItem := path.Join(composePath, compose.Name, "docker-compose.yml") - if _, err := os.Stat(pathItem); err != nil { - continue - } - upCmd := fmt.Sprintf("docker-compose -f %s up -d", pathItem) - stdout, err := cmd.Exec(upCmd) - if err != nil { - global.LOG.Debugf("%s failed, err: %v", upCmd, stdout) - } - } - global.LOG.Debug("restart all compose successful!") -} diff --git a/backend/app/service/ssh.go b/backend/app/service/ssh.go deleted file mode 100644 index 1f51b4701..000000000 --- a/backend/app/service/ssh.go +++ /dev/null @@ -1,573 +0,0 @@ -package service - -import ( - "fmt" - "os" - "os/user" - "path" - "path/filepath" - "sort" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/1Panel-dev/1Panel/backend/utils/qqwry" - "github.com/1Panel-dev/1Panel/backend/utils/systemctl" - "github.com/pkg/errors" -) - -const sshPath = "/etc/ssh/sshd_config" - -type SSHService struct{} - -type ISSHService interface { - GetSSHInfo() (*dto.SSHInfo, error) - OperateSSH(operation string) error - UpdateByFile(value string) error - Update(req dto.SSHUpdate) error - GenerateSSH(req dto.GenerateSSH) error - LoadSSHSecret(mode string) (string, error) - LoadLog(req dto.SearchSSHLog) (*dto.SSHLog, error) - - LoadSSHConf() (string, error) -} - -func NewISSHService() ISSHService { - return &SSHService{} -} - -func (u *SSHService) GetSSHInfo() (*dto.SSHInfo, error) { - data := dto.SSHInfo{ - AutoStart: true, - Status: constant.StatusEnable, - Message: "", - Port: "22", - ListenAddress: "", - PasswordAuthentication: "yes", - PubkeyAuthentication: "yes", - PermitRootLogin: "yes", - UseDNS: "yes", - } - serviceName, err := loadServiceName() - if err != nil { - data.Status = constant.StatusDisable - data.Message = err.Error() - } else { - active, err := systemctl.IsActive(serviceName) - if !active { - data.Status = constant.StatusDisable - if err != nil { - data.Message = err.Error() - } - } else { - data.Status = constant.StatusEnable - } - } - - out, err := systemctl.RunSystemCtl("is-enabled", serviceName) - if err != nil { - data.AutoStart = false - } else { - if out == "alias\n" { - data.AutoStart, _ = systemctl.IsEnable("ssh") - } else { - data.AutoStart = out == "enabled\n" - } - } - - sshConf, err := os.ReadFile(sshPath) - if err != nil { - data.Message = err.Error() - data.Status = constant.StatusDisable - } - lines := strings.Split(string(sshConf), "\n") - for _, line := range lines { - if strings.HasPrefix(line, "Port ") { - data.Port = strings.ReplaceAll(line, "Port ", "") - } - if strings.HasPrefix(line, "ListenAddress ") { - itemAddr := strings.ReplaceAll(line, "ListenAddress ", "") - if len(data.ListenAddress) != 0 { - data.ListenAddress += ("," + itemAddr) - } else { - data.ListenAddress = itemAddr - } - } - if strings.HasPrefix(line, "PasswordAuthentication ") { - data.PasswordAuthentication = strings.ReplaceAll(line, "PasswordAuthentication ", "") - } - if strings.HasPrefix(line, "PubkeyAuthentication ") { - data.PubkeyAuthentication = strings.ReplaceAll(line, "PubkeyAuthentication ", "") - } - if strings.HasPrefix(line, "PermitRootLogin ") { - data.PermitRootLogin = strings.ReplaceAll(strings.ReplaceAll(line, "PermitRootLogin ", ""), "prohibit-password", "without-password") - } - if strings.HasPrefix(line, "UseDNS ") { - data.UseDNS = strings.ReplaceAll(line, "UseDNS ", "") - } - } - return &data, nil -} - -func (u *SSHService) OperateSSH(operation string) error { - serviceName, err := loadServiceName() - if err != nil { - return err - } - sudo := cmd.SudoHandleCmd() - if operation == "enable" || operation == "disable" { - serviceName += ".service" - } - stdout, err := cmd.Execf("%s systemctl %s %s", sudo, operation, serviceName) - if err != nil { - if strings.Contains(stdout, "alias name or linked unit file") { - stdout, err := cmd.Execf("%s systemctl %s ssh", sudo, operation) - if err != nil { - return fmt.Errorf("%s ssh(alias name or linked unit file) failed, stdout: %s, err: %v", operation, stdout, err) - } - } - return fmt.Errorf("%s %s failed, stdout: %s, err: %v", operation, serviceName, stdout, err) - } - return nil -} - -func (u *SSHService) Update(req dto.SSHUpdate) error { - serviceName, err := loadServiceName() - if err != nil { - return err - } - - sshConf, err := os.ReadFile(sshPath) - if err != nil { - return err - } - lines := strings.Split(string(sshConf), "\n") - newFiles := updateSSHConf(lines, req.Key, req.NewValue) - file, err := os.OpenFile(sshPath, os.O_WRONLY|os.O_TRUNC, 0666) - if err != nil { - return err - } - defer file.Close() - if _, err = file.WriteString(strings.Join(newFiles, "\n")); err != nil { - return err - } - sudo := cmd.SudoHandleCmd() - if req.Key == "Port" { - stdout, _ := cmd.Execf("%s getenforce", sudo) - if stdout == "Enforcing\n" { - _, _ = cmd.Execf("%s semanage port -a -t ssh_port_t -p tcp %s", sudo, req.NewValue) - } - - ruleItem := dto.PortRuleUpdate{ - OldRule: dto.PortRuleOperate{ - Operation: "remove", - Port: req.OldValue, - Protocol: "tcp", - Strategy: "accept", - }, - NewRule: dto.PortRuleOperate{ - Operation: "add", - Port: req.NewValue, - Protocol: "tcp", - Strategy: "accept", - }, - } - if err := NewIFirewallService().UpdatePortRule(ruleItem); err != nil { - global.LOG.Errorf("reset firewall rules %s -> %s failed, err: %v", req.OldValue, req.NewValue, err) - } - - if err = NewIHostService().Update(1, map[string]interface{}{"port": req.NewValue}); err != nil { - global.LOG.Errorf("reset host port %s -> %s failed, err: %v", req.OldValue, req.NewValue, err) - } - } - - _, _ = cmd.Execf("%s systemctl restart %s", sudo, serviceName) - return nil -} - -func (u *SSHService) UpdateByFile(value string) error { - serviceName, err := loadServiceName() - if err != nil { - return err - } - - file, err := os.OpenFile(sshPath, os.O_WRONLY|os.O_TRUNC, 0666) - if err != nil { - return err - } - defer file.Close() - if _, err = file.WriteString(value); err != nil { - return err - } - sudo := cmd.SudoHandleCmd() - _, _ = cmd.Execf("%s systemctl restart %s", sudo, serviceName) - return nil -} - -func (u *SSHService) GenerateSSH(req dto.GenerateSSH) error { - if cmd.CheckIllegal(req.EncryptionMode, req.Password) { - return buserr.New(constant.ErrCmdIllegal) - } - currentUser, err := user.Current() - if err != nil { - return fmt.Errorf("load current user failed, err: %v", err) - } - secretFile := fmt.Sprintf("%s/.ssh/id_item_%s", currentUser.HomeDir, req.EncryptionMode) - secretPubFile := fmt.Sprintf("%s/.ssh/id_item_%s.pub", currentUser.HomeDir, req.EncryptionMode) - authFilePath := currentUser.HomeDir + "/.ssh/authorized_keys" - - command := fmt.Sprintf("ssh-keygen -t %s -f %s/.ssh/id_item_%s | echo y", req.EncryptionMode, currentUser.HomeDir, req.EncryptionMode) - if len(req.Password) != 0 { - command = fmt.Sprintf("ssh-keygen -t %s -P %s -f %s/.ssh/id_item_%s | echo y", req.EncryptionMode, req.Password, currentUser.HomeDir, req.EncryptionMode) - } - stdout, err := cmd.Exec(command) - if err != nil { - return fmt.Errorf("generate failed, err: %v, message: %s", err, stdout) - } - defer func() { - _ = os.Remove(secretFile) - }() - defer func() { - _ = os.Remove(secretPubFile) - }() - - if _, err := os.Stat(authFilePath); err != nil && errors.Is(err, os.ErrNotExist) { - authFile, err := os.Create(authFilePath) - if err != nil { - return err - } - defer authFile.Close() - } - stdout1, err := cmd.Execf("cat %s >> %s/.ssh/authorized_keys", secretPubFile, currentUser.HomeDir) - if err != nil { - return fmt.Errorf("generate failed, err: %v, message: %s", err, stdout1) - } - - fileOp := files.NewFileOp() - if err := fileOp.Rename(secretFile, fmt.Sprintf("%s/.ssh/id_%s", currentUser.HomeDir, req.EncryptionMode)); err != nil { - return err - } - if err := fileOp.Rename(secretPubFile, fmt.Sprintf("%s/.ssh/id_%s.pub", currentUser.HomeDir, req.EncryptionMode)); err != nil { - return err - } - - return nil -} - -func (u *SSHService) LoadSSHSecret(mode string) (string, error) { - currentUser, err := user.Current() - if err != nil { - return "", fmt.Errorf("load current user failed, err: %v", err) - } - - homeDir := currentUser.HomeDir - if _, err := os.Stat(fmt.Sprintf("%s/.ssh/id_%s", homeDir, mode)); err != nil { - return "", nil - } - file, err := os.ReadFile(fmt.Sprintf("%s/.ssh/id_%s", homeDir, mode)) - return string(file), err -} - -type sshFileItem struct { - Name string - Year int -} - -func (u *SSHService) LoadLog(req dto.SearchSSHLog) (*dto.SSHLog, error) { - var fileList []sshFileItem - var data dto.SSHLog - baseDir := "/var/log" - if err := filepath.Walk(baseDir, func(pathItem string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if !info.IsDir() && (strings.HasPrefix(info.Name(), "secure") || strings.HasPrefix(info.Name(), "auth")) { - if !strings.HasSuffix(info.Name(), ".gz") { - fileList = append(fileList, sshFileItem{Name: pathItem, Year: info.ModTime().Year()}) - return nil - } - itemFileName := strings.TrimSuffix(pathItem, ".gz") - if _, err := os.Stat(itemFileName); err != nil && os.IsNotExist(err) { - if err := handleGunzip(pathItem); err == nil { - fileList = append(fileList, sshFileItem{Name: itemFileName, Year: info.ModTime().Year()}) - } - } - } - return nil - }); err != nil { - return nil, err - } - fileList = sortFileList(fileList) - - command := "" - if len(req.Info) != 0 { - command = fmt.Sprintf(" | grep '%s'", req.Info) - } - - showCountFrom := (req.Page - 1) * req.PageSize - showCountTo := req.Page * req.PageSize - nyc, _ := time.LoadLocation(common.LoadTimeZone()) - qqWry, err := qqwry.NewQQwry() - if err != nil { - global.LOG.Errorf("load qqwry datas failed: %s", err) - } - for _, file := range fileList { - commandItem := "" - if strings.HasPrefix(path.Base(file.Name), "secure") { - switch req.Status { - case constant.StatusSuccess: - commandItem = fmt.Sprintf("cat %s | grep -a Accepted %s", file.Name, command) - case constant.StatusFailed: - commandItem = fmt.Sprintf("cat %s | grep -a 'Failed password for' %s", file.Name, command) - default: - commandItem = fmt.Sprintf("cat %s | grep -aE '(Failed password for|Accepted)' %s", file.Name, command) - } - } - if strings.HasPrefix(path.Base(file.Name), "auth.log") { - switch req.Status { - case constant.StatusSuccess: - commandItem = fmt.Sprintf("cat %s | grep -a Accepted %s", file.Name, command) - case constant.StatusFailed: - commandItem = fmt.Sprintf("cat %s | grep -aE 'Failed password for|Connection closed by authenticating user' %s", file.Name, command) - default: - commandItem = fmt.Sprintf("cat %s | grep -aE \"(Failed password for|Connection closed by authenticating user|Accepted)\" %s", file.Name, command) - } - } - dataItem, successCount, failedCount := loadSSHData(commandItem, showCountFrom, showCountTo, file.Year, qqWry, nyc) - data.FailedCount += failedCount - data.TotalCount += successCount + failedCount - showCountFrom = showCountFrom - (successCount + failedCount) - showCountTo = showCountTo - (successCount + failedCount) - data.Logs = append(data.Logs, dataItem...) - } - - data.SuccessfulCount = data.TotalCount - data.FailedCount - return &data, nil -} - -func (u *SSHService) LoadSSHConf() (string, error) { - if _, err := os.Stat("/etc/ssh/sshd_config"); err != nil { - return "", buserr.New("ErrHttpReqNotFound") - } - content, err := os.ReadFile("/etc/ssh/sshd_config") - if err != nil { - return "", err - } - return string(content), nil -} - -func sortFileList(fileNames []sshFileItem) []sshFileItem { - if len(fileNames) < 2 { - return fileNames - } - if strings.HasPrefix(path.Base(fileNames[0].Name), "secure") { - var itemFile []sshFileItem - sort.Slice(fileNames, func(i, j int) bool { - return fileNames[i].Name > fileNames[j].Name - }) - itemFile = append(itemFile, fileNames[len(fileNames)-1]) - itemFile = append(itemFile, fileNames[:len(fileNames)-1]...) - return itemFile - } - sort.Slice(fileNames, func(i, j int) bool { - return fileNames[i].Name < fileNames[j].Name - }) - return fileNames -} - -func updateSSHConf(oldFiles []string, param string, value string) []string { - var valueItems []string - if param != "ListenAddress" { - valueItems = append(valueItems, value) - } else { - if value != "" { - valueItems = strings.Split(value, ",") - } - } - var newFiles []string - for _, line := range oldFiles { - lineItem := strings.TrimSpace(line) - if (strings.HasPrefix(lineItem, param) || strings.HasPrefix(lineItem, fmt.Sprintf("#%s", param))) && len(valueItems) != 0 { - newFiles = append(newFiles, fmt.Sprintf("%s %s", param, valueItems[0])) - valueItems = valueItems[1:] - continue - } - if strings.HasPrefix(lineItem, param) && len(valueItems) == 0 { - newFiles = append(newFiles, fmt.Sprintf("#%s", line)) - continue - } - newFiles = append(newFiles, line) - } - if len(valueItems) != 0 { - for _, item := range valueItems { - newFiles = append(newFiles, fmt.Sprintf("%s %s", param, item)) - } - } - return newFiles -} - -func loadSSHData(command string, showCountFrom, showCountTo, currentYear int, qqWry *qqwry.QQwry, nyc *time.Location) ([]dto.SSHHistory, int, int) { - var ( - datas []dto.SSHHistory - successCount int - failedCount int - ) - stdout2, err := cmd.Exec(command) - if err != nil { - return datas, 0, 0 - } - lines := strings.Split(string(stdout2), "\n") - for i := len(lines) - 1; i >= 0; i-- { - var itemData dto.SSHHistory - switch { - case strings.Contains(lines[i], "Failed password for"): - itemData = loadFailedSecureDatas(lines[i]) - if len(itemData.Address) != 0 { - if successCount+failedCount >= showCountFrom && successCount+failedCount < showCountTo { - itemData.Area = qqWry.Find(itemData.Address).Area - itemData.Date = loadDate(currentYear, itemData.DateStr, nyc) - datas = append(datas, itemData) - } - failedCount++ - } - case strings.Contains(lines[i], "Connection closed by authenticating user"): - itemData = loadFailedAuthDatas(lines[i]) - if len(itemData.Address) != 0 { - if successCount+failedCount >= showCountFrom && successCount+failedCount < showCountTo { - itemData.Area = qqWry.Find(itemData.Address).Area - itemData.Date = loadDate(currentYear, itemData.DateStr, nyc) - datas = append(datas, itemData) - } - failedCount++ - } - case strings.Contains(lines[i], "Accepted "): - itemData = loadSuccessDatas(lines[i]) - if len(itemData.Address) != 0 { - if successCount+failedCount >= showCountFrom && successCount+failedCount < showCountTo { - itemData.Area = qqWry.Find(itemData.Address).Area - itemData.Date = loadDate(currentYear, itemData.DateStr, nyc) - datas = append(datas, itemData) - } - successCount++ - } - } - } - return datas, successCount, failedCount -} - -func loadSuccessDatas(line string) dto.SSHHistory { - var data dto.SSHHistory - parts := strings.Fields(line) - index, dataStr := analyzeDateStr(parts) - if dataStr == "" { - return data - } - data.DateStr = dataStr - data.AuthMode = parts[4+index] - data.User = parts[6+index] - data.Address = parts[8+index] - data.Port = parts[10+index] - data.Status = constant.StatusSuccess - return data -} - -func loadFailedAuthDatas(line string) dto.SSHHistory { - var data dto.SSHHistory - parts := strings.Fields(line) - index, dataStr := analyzeDateStr(parts) - if dataStr == "" { - return data - } - data.DateStr = dataStr - switch index { - case 1: - data.User = parts[9] - case 2: - data.User = parts[10] - default: - data.User = parts[7] - } - data.AuthMode = parts[6+index] - data.Address = parts[9+index] - data.Port = parts[11+index] - data.Status = constant.StatusFailed - if strings.Contains(line, ": ") { - data.Message = strings.Split(line, ": ")[1] - } - return data -} -func loadFailedSecureDatas(line string) dto.SSHHistory { - var data dto.SSHHistory - parts := strings.Fields(line) - index, dataStr := analyzeDateStr(parts) - if dataStr == "" { - return data - } - data.DateStr = dataStr - if strings.Contains(line, " invalid ") { - data.AuthMode = parts[4+index] - index += 2 - } else { - data.AuthMode = parts[4+index] - } - data.User = parts[6+index] - data.Address = parts[8+index] - data.Port = parts[10+index] - data.Status = constant.StatusFailed - if strings.Contains(line, ": ") { - data.Message = strings.Split(line, ": ")[1] - } - return data -} - -func handleGunzip(path string) error { - if _, err := cmd.Execf("gunzip %s", path); err != nil { - return err - } - return nil -} - -func loadServiceName() (string, error) { - if exist, _ := systemctl.IsExist("sshd"); exist { - return "sshd", nil - } else if exist, _ := systemctl.IsExist("ssh"); exist { - return "ssh", nil - } - return "", errors.New("The ssh or sshd service is unavailable") -} - -func loadDate(currentYear int, DateStr string, nyc *time.Location) time.Time { - itemDate, err := time.ParseInLocation("2006 Jan 2 15:04:05", fmt.Sprintf("%d %s", currentYear, DateStr), nyc) - if err != nil { - itemDate, _ = time.ParseInLocation("2006 Jan 2 15:04:05", DateStr, nyc) - } - return itemDate -} - -func analyzeDateStr(parts []string) (int, string) { - t, err := time.Parse(time.RFC3339Nano, parts[0]) - if err == nil { - if len(parts) < 12 { - return 0, "" - } - return 0, t.Format("2006 Jan 2 15:04:05") - } - t, err = time.Parse(constant.DateTimeLayout, fmt.Sprintf("%s %s", parts[0], parts[1])) - if err == nil { - if len(parts) < 14 { - return 0, "" - } - return 1, t.Format("2006 Jan 2 15:04:05") - } - - if len(parts) < 14 { - return 0, "" - } - return 2, fmt.Sprintf("%s %s %s", parts[0], parts[1], parts[2]) -} diff --git a/backend/app/service/website.go b/backend/app/service/website.go deleted file mode 100644 index f62ababff..000000000 --- a/backend/app/service/website.go +++ /dev/null @@ -1,2565 +0,0 @@ -package service - -import ( - "bufio" - "bytes" - "context" - "crypto/x509" - "encoding/json" - "encoding/pem" - "errors" - "fmt" - "os" - "path" - "reflect" - "regexp" - "strconv" - "strings" - "syscall" - "time" - - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/jinzhu/copier" - - "github.com/1Panel-dev/1Panel/backend/i18n" - "github.com/spf13/afero" - - "github.com/1Panel-dev/1Panel/backend/utils/compose" - "github.com/1Panel-dev/1Panel/backend/utils/env" - - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/nginx" - "github.com/1Panel-dev/1Panel/backend/utils/nginx/components" - "github.com/1Panel-dev/1Panel/backend/utils/nginx/parser" - "github.com/1Panel-dev/1Panel/cmd/server/nginx_conf" - "golang.org/x/crypto/bcrypt" - "gopkg.in/ini.v1" - "gorm.io/gorm" - - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/app/dto/response" - "github.com/1Panel-dev/1Panel/backend/app/repo" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/global" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/files" -) - -type WebsiteService struct { -} - -type IWebsiteService interface { - PageWebsite(req request.WebsiteSearch) (int64, []response.WebsiteRes, error) - GetWebsites() ([]response.WebsiteDTO, error) - CreateWebsite(create request.WebsiteCreate) error - OpWebsite(req request.WebsiteOp) error - GetWebsiteOptions() ([]response.WebsiteOption, error) - UpdateWebsite(req request.WebsiteUpdate) error - DeleteWebsite(req request.WebsiteDelete) error - GetWebsite(id uint) (response.WebsiteDTO, error) - - CreateWebsiteDomain(create request.WebsiteDomainCreate) ([]model.WebsiteDomain, error) - GetWebsiteDomain(websiteId uint) ([]model.WebsiteDomain, error) - DeleteWebsiteDomain(domainId uint) error - - GetNginxConfigByScope(req request.NginxScopeReq) (*response.WebsiteNginxConfig, error) - UpdateNginxConfigByScope(req request.NginxConfigUpdate) error - GetWebsiteNginxConfig(websiteId uint, configType string) (response.FileInfo, error) - UpdateNginxConfigFile(req request.WebsiteNginxUpdate) error - GetWebsiteHTTPS(websiteId uint) (response.WebsiteHTTPS, error) - OpWebsiteHTTPS(ctx context.Context, req request.WebsiteHTTPSOp) (*response.WebsiteHTTPS, error) - OpWebsiteLog(req request.WebsiteLogReq) (*response.WebsiteLog, error) - ChangeDefaultServer(id uint) error - PreInstallCheck(req request.WebsiteInstallCheckReq) ([]response.WebsitePreInstallCheck, error) - - GetPHPConfig(id uint) (*response.PHPConfig, error) - UpdatePHPConfig(req request.WebsitePHPConfigUpdate) error - UpdatePHPConfigFile(req request.WebsitePHPFileUpdate) error - ChangePHPVersion(req request.WebsitePHPVersionReq) error - - GetRewriteConfig(req request.NginxRewriteReq) (*response.NginxRewriteRes, error) - UpdateRewriteConfig(req request.NginxRewriteUpdate) error - LoadWebsiteDirConfig(req request.WebsiteCommonReq) (*response.WebsiteDirConfig, error) - UpdateSiteDir(req request.WebsiteUpdateDir) error - UpdateSitePermission(req request.WebsiteUpdateDirPermission) error - OperateProxy(req request.WebsiteProxyConfig) (err error) - GetProxies(id uint) (res []request.WebsiteProxyConfig, err error) - UpdateProxyFile(req request.NginxProxyUpdate) (err error) - GetAuthBasics(req request.NginxAuthReq) (res response.NginxAuthRes, err error) - UpdateAuthBasic(req request.NginxAuthUpdate) (err error) - GetAntiLeech(id uint) (*response.NginxAntiLeechRes, error) - UpdateAntiLeech(req request.NginxAntiLeechUpdate) (err error) - OperateRedirect(req request.NginxRedirectReq) (err error) - GetRedirect(id uint) (res []response.NginxRedirectConfig, err error) - UpdateRedirectFile(req request.NginxRedirectUpdate) (err error) - - UpdateDefaultHtml(req request.WebsiteHtmlUpdate) error - GetDefaultHtml(resourceType string) (*response.WebsiteHtmlRes, error) -} - -func NewIWebsiteService() IWebsiteService { - return &WebsiteService{} -} - -func (w WebsiteService) PageWebsite(req request.WebsiteSearch) (int64, []response.WebsiteRes, error) { - var ( - websiteDTOs []response.WebsiteRes - opts []repo.DBOption - ) - nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return 0, nil, nil - } - return 0, nil, err - } - opts = append(opts, commonRepo.WithOrderRuleBy(req.OrderBy, req.Order)) - if req.Name != "" { - domains, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithDomainLike(req.Name)) - if len(domains) > 0 { - var websiteIds []uint - for _, domain := range domains { - websiteIds = append(websiteIds, domain.WebsiteID) - } - opts = append(opts, websiteRepo.WithIDs(websiteIds)) - } else { - opts = append(opts, websiteRepo.WithDomainLike(req.Name)) - } - } - if req.WebsiteGroupID != 0 { - opts = append(opts, websiteRepo.WithGroupID(req.WebsiteGroupID)) - } - total, websites, err := websiteRepo.Page(req.Page, req.PageSize, opts...) - if err != nil { - return 0, nil, err - } - for _, web := range websites { - var ( - appName string - runtimeName string - ) - switch web.Type { - case constant.Deployment: - appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(web.AppInstallID)) - if err != nil { - return 0, nil, err - } - appName = appInstall.Name - case constant.Runtime: - runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(web.RuntimeID)) - if err != nil { - return 0, nil, err - } - runtimeName = runtime.Name - } - sitePath := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "www", "sites", web.Alias) - - websiteDTOs = append(websiteDTOs, response.WebsiteRes{ - ID: web.ID, - CreatedAt: web.CreatedAt, - Protocol: web.Protocol, - PrimaryDomain: web.PrimaryDomain, - Type: web.Type, - Remark: web.Remark, - Status: web.Status, - Alias: web.Alias, - AppName: appName, - ExpireDate: web.ExpireDate, - SSLExpireDate: web.WebsiteSSL.ExpireDate, - SSLStatus: checkSSLStatus(web.WebsiteSSL.ExpireDate), - RuntimeName: runtimeName, - SitePath: sitePath, - }) - } - return total, websiteDTOs, nil -} - -func (w WebsiteService) GetWebsites() ([]response.WebsiteDTO, error) { - var websiteDTOs []response.WebsiteDTO - websites, err := websiteRepo.List(commonRepo.WithOrderRuleBy("primary_domain", "ascending")) - if err != nil { - return nil, err - } - for _, web := range websites { - websiteDTOs = append(websiteDTOs, response.WebsiteDTO{ - Website: web, - }) - } - return websiteDTOs, nil -} - -func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) { - alias := create.Alias - if alias == "default" { - return buserr.New("ErrDefaultAlias") - } - if common.ContainsChinese(alias) { - alias, err = common.PunycodeEncode(alias) - if err != nil { - return - } - } - if exist, _ := websiteRepo.GetBy(websiteRepo.WithAlias(alias)); len(exist) > 0 { - return buserr.New(constant.ErrAliasIsExist) - } - - nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return err - } - defaultHttpPort := nginxInstall.HttpPort - - var ( - otherDomains []model.WebsiteDomain - domains []model.WebsiteDomain - ) - domains, _, _, err = getWebsiteDomains(create.PrimaryDomain, defaultHttpPort, 0) - if err != nil { - return err - } - otherDomains, _, _, err = getWebsiteDomains(create.OtherDomains, defaultHttpPort, 0) - if err != nil { - return err - } - domains = append(domains, otherDomains...) - - defaultDate, _ := time.Parse(constant.DateLayout, constant.DefaultDate) - website := &model.Website{ - PrimaryDomain: create.PrimaryDomain, - Type: create.Type, - Alias: alias, - Remark: create.Remark, - Status: constant.WebRunning, - ExpireDate: defaultDate, - WebsiteGroupID: create.WebsiteGroupID, - Protocol: constant.ProtocolHTTP, - Proxy: create.Proxy, - SiteDir: "/", - AccessLog: true, - ErrorLog: true, - IPV6: create.IPV6, - } - - var ( - appInstall *model.AppInstall - runtime *model.Runtime - ) - - defer func() { - if err != nil { - if website.AppInstallID > 0 { - req := request.AppInstalledOperate{ - InstallId: website.AppInstallID, - Operate: constant.Delete, - ForceDelete: true, - } - if err := NewIAppInstalledService().Operate(req); err != nil { - global.LOG.Errorf(err.Error()) - } - } - } - }() - var proxy string - - switch create.Type { - case constant.Deployment: - if create.AppType == constant.NewApp { - var ( - req request.AppInstallCreate - install *model.AppInstall - ) - req.Name = create.AppInstall.Name - req.AppDetailId = create.AppInstall.AppDetailId - req.Params = create.AppInstall.Params - req.AppContainerConfig = create.AppInstall.AppContainerConfig - tx, installCtx := getTxAndContext() - install, err = NewIAppService().Install(installCtx, req) - if err != nil { - tx.Rollback() - return err - } - tx.Commit() - appInstall = install - website.AppInstallID = install.ID - website.Proxy = fmt.Sprintf("127.0.0.1:%d", appInstall.HttpPort) - } else { - var install model.AppInstall - install, err = appInstallRepo.GetFirst(commonRepo.WithByID(create.AppInstallID)) - if err != nil { - return err - } - appInstall = &install - website.AppInstallID = appInstall.ID - website.Proxy = fmt.Sprintf("127.0.0.1:%d", appInstall.HttpPort) - } - case constant.Runtime: - runtime, err = runtimeRepo.GetFirst(commonRepo.WithByID(create.RuntimeID)) - if err != nil { - return err - } - website.RuntimeID = runtime.ID - switch runtime.Type { - case constant.RuntimePHP: - if runtime.Resource == constant.ResourceAppstore { - var ( - req request.AppInstallCreate - install *model.AppInstall - ) - reg, _ := regexp.Compile(`[^a-z0-9_-]+`) - req.Name = reg.ReplaceAllString(strings.ToLower(alias), "") - req.AppDetailId = create.AppInstall.AppDetailId - req.Params = create.AppInstall.Params - req.Params["IMAGE_NAME"] = runtime.Image - req.AppContainerConfig = create.AppInstall.AppContainerConfig - req.Params["PANEL_WEBSITE_DIR"] = path.Join(nginxInstall.GetPath(), "/www") - tx, installCtx := getTxAndContext() - install, err = NewIAppService().Install(installCtx, req) - if err != nil { - tx.Rollback() - return err - } - tx.Commit() - website.AppInstallID = install.ID - appInstall = install - website.Proxy = fmt.Sprintf("127.0.0.1:%d", appInstall.HttpPort) - } else { - website.ProxyType = create.ProxyType - if website.ProxyType == constant.RuntimeProxyUnix { - proxy = fmt.Sprintf("unix:%s", path.Join("/www/sites", website.Alias, "php-pool", "php-fpm.sock")) - } - if website.ProxyType == constant.RuntimeProxyTcp { - proxy = fmt.Sprintf("127.0.0.1:%d", create.Port) - } - website.Proxy = proxy - } - case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo: - website.Proxy = fmt.Sprintf("127.0.0.1:%d", runtime.Port) - } - } - - if err = configDefaultNginx(website, domains, appInstall, runtime); err != nil { - return err - } - - if len(create.FtpUser) != 0 && len(create.FtpPassword) != 0 { - indexDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "index") - itemID, err := NewIFtpService().Create(dto.FtpCreate{User: create.FtpUser, Password: create.FtpPassword, Path: indexDir}) - if err != nil { - global.LOG.Errorf("create ftp for website failed, err: %v", err) - } - website.FtpID = itemID - } - - if err = createWafConfig(website, domains); err != nil { - return err - } - - tx, ctx := helper.GetTxAndContext() - defer tx.Rollback() - if err = websiteRepo.Create(ctx, website); err != nil { - return err - } - for i := range domains { - domains[i].WebsiteID = website.ID - } - if err = websiteDomainRepo.BatchCreate(ctx, domains); err != nil { - return err - } - tx.Commit() - return nil -} - -func (w WebsiteService) OpWebsite(req request.WebsiteOp) error { - website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) - if err != nil { - return err - } - if err := opWebsite(&website, req.Operate); err != nil { - return err - } - return websiteRepo.Save(context.Background(), &website) -} - -func (w WebsiteService) GetWebsiteOptions() ([]response.WebsiteOption, error) { - webs, err := websiteRepo.List() - if err != nil { - return nil, err - } - var datas []response.WebsiteOption - for _, web := range webs { - var item response.WebsiteOption - if err := copier.Copy(&item, &web); err != nil { - return nil, err - } - datas = append(datas, item) - } - return datas, nil -} - -func (w WebsiteService) UpdateWebsite(req request.WebsiteUpdate) error { - website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) - if err != nil { - return err - } - if website.IPV6 != req.IPV6 { - if err := changeIPV6(website, req.IPV6); err != nil { - return err - } - } - website.PrimaryDomain = req.PrimaryDomain - website.WebsiteGroupID = req.WebsiteGroupID - website.Remark = req.Remark - website.IPV6 = req.IPV6 - - if req.ExpireDate != "" { - expireDate, err := time.Parse(constant.DateLayout, req.ExpireDate) - if err != nil { - return err - } - website.ExpireDate = expireDate - } - - return websiteRepo.Save(context.TODO(), &website) -} - -func (w WebsiteService) GetWebsite(id uint) (response.WebsiteDTO, error) { - var res response.WebsiteDTO - website, err := websiteRepo.GetFirst(commonRepo.WithByID(id)) - if err != nil { - return res, err - } - res.Website = website - - nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return res, err - } - sitePath := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "www", "sites", website.Alias) - res.ErrorLogPath = path.Join(sitePath, "log", "error.log") - res.AccessLogPath = path.Join(sitePath, "log", "access.log") - res.SitePath = sitePath - res.SiteDir = website.SiteDir - return res, nil -} - -func (w WebsiteService) DeleteWebsite(req request.WebsiteDelete) error { - website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) - if err != nil { - return err - } - if err = delNginxConfig(website, req.ForceDelete); err != nil { - return err - } - - if err = delWafConfig(website, req.ForceDelete); err != nil { - return err - } - - if checkIsLinkApp(website) && req.DeleteApp { - appInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) - if appInstall.ID > 0 { - if err = deleteAppInstall(appInstall, true, req.ForceDelete, true); err != nil && !req.ForceDelete { - return err - } - } - } - - tx, ctx := helper.GetTxAndContext() - defer tx.Rollback() - - go func() { - _ = NewIBackupService().DeleteRecordByName("website", website.PrimaryDomain, website.Alias, req.DeleteBackup) - }() - - if err := websiteRepo.DeleteBy(ctx, commonRepo.WithByID(req.ID)); err != nil { - return err - } - if err := websiteDomainRepo.DeleteBy(ctx, websiteDomainRepo.WithWebsiteId(req.ID)); err != nil { - return err - } - tx.Commit() - - uploadDir := path.Join(global.CONF.System.BaseDir, fmt.Sprintf("1panel/uploads/website/%s", website.Alias)) - if _, err := os.Stat(uploadDir); err == nil { - _ = os.RemoveAll(uploadDir) - } - return nil -} - -func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate) ([]model.WebsiteDomain, error) { - var ( - domainModels []model.WebsiteDomain - addPorts []int - addDomains []string - ) - httpPort, _, err := getAppInstallPort(constant.AppOpenresty) - if err != nil { - return nil, err - } - website, err := websiteRepo.GetFirst(commonRepo.WithByID(create.WebsiteID)) - if err != nil { - return nil, err - } - - domainModels, addPorts, addDomains, err = getWebsiteDomains(create.Domains, httpPort, create.WebsiteID) - if err != nil { - return nil, err - } - go func() { - _ = OperateFirewallPort(nil, addPorts) - }() - - if err := addListenAndServerName(website, addPorts, addDomains); err != nil { - return nil, err - } - - nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return nil, err - } - wafDataPath := path.Join(nginxInstall.GetPath(), "1pwaf", "data") - fileOp := files.NewFileOp() - if fileOp.Stat(wafDataPath) { - websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json") - content, err := fileOp.GetContent(websitesConfigPath) - if err != nil { - return nil, err - } - var websitesArray []request.WafWebsite - if content != nil { - if err := json.Unmarshal(content, &websitesArray); err != nil { - return nil, err - } - } - for index, wafWebsite := range websitesArray { - if wafWebsite.Key == website.Alias { - wafSite := request.WafWebsite{ - Key: website.Alias, - Domains: wafWebsite.Domains, - Host: wafWebsite.Host, - } - for _, domain := range domainModels { - wafSite.Domains = append(wafSite.Domains, domain.Domain) - wafSite.Host = append(wafSite.Host, domain.Domain+":"+strconv.Itoa(domain.Port)) - } - if len(wafSite.Host) == 0 { - wafSite.Host = []string{} - } - websitesArray[index] = wafSite - break - } - } - websitesContent, err := json.Marshal(websitesArray) - if err != nil { - return nil, err - } - if err := fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, 0644); err != nil { - return nil, err - } - } - - return domainModels, websiteDomainRepo.BatchCreate(context.TODO(), domainModels) -} - -func (w WebsiteService) GetWebsiteDomain(websiteId uint) ([]model.WebsiteDomain, error) { - return websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(websiteId)) -} - -func (w WebsiteService) DeleteWebsiteDomain(domainId uint) error { - webSiteDomain, err := websiteDomainRepo.GetFirst(commonRepo.WithByID(domainId)) - if err != nil { - return err - } - - if websiteDomains, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(webSiteDomain.WebsiteID)); len(websiteDomains) == 1 { - return fmt.Errorf("can not delete last domain") - } - website, err := websiteRepo.GetFirst(commonRepo.WithByID(webSiteDomain.WebsiteID)) - if err != nil { - return err - } - var ports []int - if oldDomains, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(webSiteDomain.WebsiteID), websiteDomainRepo.WithPort(webSiteDomain.Port)); len(oldDomains) == 1 { - ports = append(ports, webSiteDomain.Port) - } - - var domains []string - if oldDomains, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(webSiteDomain.WebsiteID), websiteDomainRepo.WithDomain(webSiteDomain.Domain)); len(oldDomains) == 1 { - domains = append(domains, webSiteDomain.Domain) - } - - if len(ports) > 0 || len(domains) > 0 { - stringBinds := make([]string, len(ports)) - for i := 0; i < len(ports); i++ { - stringBinds[i] = strconv.Itoa(ports[i]) - } - if err := deleteListenAndServerName(website, stringBinds, domains); err != nil { - return err - } - } - - nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return err - } - wafDataPath := path.Join(nginxInstall.GetPath(), "1pwaf", "data") - fileOp := files.NewFileOp() - if fileOp.Stat(wafDataPath) { - websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json") - content, err := fileOp.GetContent(websitesConfigPath) - if err != nil { - return err - } - var websitesArray []request.WafWebsite - var newWebsitesArray []request.WafWebsite - if content != nil { - if err := json.Unmarshal(content, &websitesArray); err != nil { - return err - } - } - for _, wafWebsite := range websitesArray { - if wafWebsite.Key == website.Alias { - wafSite := wafWebsite - oldDomains := wafSite.Domains - var newDomains []string - for _, domain := range oldDomains { - if domain == webSiteDomain.Domain { - continue - } - newDomains = append(newDomains, domain) - } - wafSite.Domains = newDomains - oldHostArray := wafSite.Host - var newHostArray []string - for _, host := range oldHostArray { - if host == webSiteDomain.Domain+":"+strconv.Itoa(webSiteDomain.Port) { - continue - } - newHostArray = append(newHostArray, host) - } - wafSite.Host = newHostArray - if len(wafSite.Host) == 0 { - wafSite.Host = []string{} - } - newWebsitesArray = append(newWebsitesArray, wafSite) - } else { - newWebsitesArray = append(newWebsitesArray, wafWebsite) - } - } - websitesContent, err := json.Marshal(newWebsitesArray) - if err != nil { - return err - } - if err = fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, 0644); err != nil { - return err - } - } - - return websiteDomainRepo.DeleteBy(context.TODO(), commonRepo.WithByID(domainId)) -} - -func (w WebsiteService) GetNginxConfigByScope(req request.NginxScopeReq) (*response.WebsiteNginxConfig, error) { - keys, ok := dto.ScopeKeyMap[req.Scope] - if !ok || len(keys) == 0 { - return nil, nil - } - - website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) - if err != nil { - return nil, err - } - var config response.WebsiteNginxConfig - params, err := getNginxParamsByKeys(constant.NginxScopeServer, keys, &website) - if err != nil { - return nil, err - } - config.Params = params - config.Enable = len(params[0].Params) > 0 - - return &config, nil -} - -func (w WebsiteService) UpdateNginxConfigByScope(req request.NginxConfigUpdate) error { - keys, ok := dto.ScopeKeyMap[req.Scope] - if !ok || len(keys) == 0 { - return nil - } - website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) - if err != nil { - return err - } - if req.Operate == constant.ConfigDel { - var nginxParams []dto.NginxParam - for _, key := range keys { - nginxParams = append(nginxParams, dto.NginxParam{ - Name: key, - }) - } - return deleteNginxConfig(constant.NginxScopeServer, nginxParams, &website) - } - params := getNginxParams(req.Params, keys) - if req.Operate == constant.ConfigNew { - if _, ok := dto.StaticFileKeyMap[req.Scope]; ok { - params = getNginxParamsFromStaticFile(req.Scope, params) - } - } - return updateNginxConfig(constant.NginxScopeServer, params, &website) -} - -func (w WebsiteService) GetWebsiteNginxConfig(websiteId uint, configType string) (response.FileInfo, error) { - website, err := websiteRepo.GetFirst(commonRepo.WithByID(websiteId)) - if err != nil { - return response.FileInfo{}, err - } - configPath := "" - switch configType { - case constant.AppOpenresty: - nginxApp, err := appRepo.GetFirst(appRepo.WithKey(constant.AppOpenresty)) - if err != nil { - return response.FileInfo{}, err - } - nginxInstall, err := appInstallRepo.GetFirst(appInstallRepo.WithAppId(nginxApp.ID)) - if err != nil { - return response.FileInfo{}, err - } - configPath = path.Join(nginxInstall.GetPath(), "conf", "conf.d", website.Alias+".conf") - case constant.ConfigFPM: - runtimeInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) - if err != nil { - return response.FileInfo{}, err - } - configPath = path.Join(runtimeInstall.GetPath(), "conf", "php-fpm.conf") - case constant.ConfigPHP: - runtimeInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) - if err != nil { - return response.FileInfo{}, err - } - configPath = path.Join(runtimeInstall.GetPath(), "conf", "php.ini") - } - info, err := files.NewFileInfo(files.FileOption{ - Path: configPath, - Expand: true, - }) - if err != nil { - return response.FileInfo{}, err - } - return response.FileInfo{FileInfo: *info}, nil -} - -func (w WebsiteService) GetWebsiteHTTPS(websiteId uint) (response.WebsiteHTTPS, error) { - website, err := websiteRepo.GetFirst(commonRepo.WithByID(websiteId)) - if err != nil { - return response.WebsiteHTTPS{}, err - } - var res response.WebsiteHTTPS - if website.WebsiteSSLID == 0 { - res.Enable = false - return res, nil - } - websiteSSL, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(website.WebsiteSSLID)) - if err != nil { - return response.WebsiteHTTPS{}, err - } - res.SSL = *websiteSSL - res.Enable = true - if website.HttpConfig != "" { - res.HttpConfig = website.HttpConfig - } else { - res.HttpConfig = constant.HTTPToHTTPS - } - params, err := getNginxParamsByKeys(constant.NginxScopeServer, []string{"ssl_protocols", "ssl_ciphers", "add_header"}, &website) - if err != nil { - return res, err - } - for _, p := range params { - if p.Name == "ssl_protocols" { - res.SSLProtocol = p.Params - } - if p.Name == "ssl_ciphers" { - res.Algorithm = p.Params[0] - } - if p.Name == "add_header" && len(p.Params) > 0 && p.Params[0] == "Strict-Transport-Security" { - res.Hsts = true - } - } - return res, nil -} - -func (w WebsiteService) OpWebsiteHTTPS(ctx context.Context, req request.WebsiteHTTPSOp) (*response.WebsiteHTTPS, error) { - website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) - if err != nil { - return nil, err - } - var ( - res response.WebsiteHTTPS - websiteSSL model.WebsiteSSL - ) - nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return nil, err - } - if err = ChangeHSTSConfig(req.Hsts, nginxInstall, website); err != nil { - return nil, err - } - res.Enable = req.Enable - res.SSLProtocol = req.SSLProtocol - res.Algorithm = req.Algorithm - if !req.Enable { - website.Protocol = constant.ProtocolHTTP - website.WebsiteSSLID = 0 - _, httpsPort, err := getAppInstallPort(constant.AppOpenresty) - if err != nil { - return nil, err - } - httpsPortStr := strconv.Itoa(httpsPort) - if err := deleteListenAndServerName(website, []string{httpsPortStr, "[::]:" + httpsPortStr}, []string{}); err != nil { - return nil, err - } - nginxParams := getNginxParamsFromStaticFile(dto.SSL, nil) - nginxParams = append(nginxParams, - dto.NginxParam{ - Name: "if", - Params: []string{"($scheme", "=", "http)"}, - }, - dto.NginxParam{ - Name: "ssl_certificate", - }, - dto.NginxParam{ - Name: "ssl_certificate_key", - }, - dto.NginxParam{ - Name: "ssl_protocols", - }, - dto.NginxParam{ - Name: "ssl_ciphers", - }, - ) - if err = deleteNginxConfig(constant.NginxScopeServer, nginxParams, &website); err != nil { - return nil, err - } - if err = websiteRepo.Save(ctx, &website); err != nil { - return nil, err - } - return nil, nil - } - - if req.Type == constant.SSLExisted { - websiteModel, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(req.WebsiteSSLID)) - if err != nil { - return nil, err - } - website.WebsiteSSLID = websiteModel.ID - res.SSL = *websiteModel - websiteSSL = *websiteModel - } - if req.Type == constant.SSLManual { - var ( - certificate string - privateKey string - ) - switch req.ImportType { - case "paste": - certificate = req.Certificate - privateKey = req.PrivateKey - case "local": - fileOp := files.NewFileOp() - if !fileOp.Stat(req.PrivateKeyPath) { - return nil, buserr.New("ErrSSLKeyNotFound") - } - if !fileOp.Stat(req.CertificatePath) { - return nil, buserr.New("ErrSSLCertificateNotFound") - } - if content, err := fileOp.GetContent(req.PrivateKeyPath); err != nil { - return nil, err - } else { - privateKey = string(content) - } - if content, err := fileOp.GetContent(req.CertificatePath); err != nil { - return nil, err - } else { - certificate = string(content) - } - } - - privateKeyCertBlock, _ := pem.Decode([]byte(privateKey)) - if privateKeyCertBlock == nil { - return nil, buserr.New("ErrSSLKeyFormat") - } - - certBlock, _ := pem.Decode([]byte(certificate)) - if certBlock == nil { - return nil, buserr.New("ErrSSLCertificateFormat") - } - cert, err := x509.ParseCertificate(certBlock.Bytes) - if err != nil { - return nil, err - } - websiteSSL.ExpireDate = cert.NotAfter - websiteSSL.StartDate = cert.NotBefore - websiteSSL.Type = cert.Issuer.CommonName - if len(cert.Issuer.Organization) > 0 { - websiteSSL.Organization = cert.Issuer.Organization[0] - } else { - websiteSSL.Organization = cert.Issuer.CommonName - } - if len(cert.DNSNames) > 0 { - websiteSSL.PrimaryDomain = cert.DNSNames[0] - websiteSSL.Domains = strings.Join(cert.DNSNames, ",") - } - websiteSSL.Provider = constant.Manual - websiteSSL.PrivateKey = privateKey - websiteSSL.Pem = certificate - - res.SSL = websiteSSL - } - - website.Protocol = constant.ProtocolHTTPS - if err := applySSL(website, websiteSSL, req); err != nil { - return nil, err - } - website.HttpConfig = req.HttpConfig - - if websiteSSL.ID == 0 { - if err := websiteSSLRepo.Create(ctx, &websiteSSL); err != nil { - return nil, err - } - website.WebsiteSSLID = websiteSSL.ID - } - if err := websiteRepo.Save(ctx, &website); err != nil { - return nil, err - } - return &res, nil -} - -func (w WebsiteService) PreInstallCheck(req request.WebsiteInstallCheckReq) ([]response.WebsitePreInstallCheck, error) { - var ( - res []response.WebsitePreInstallCheck - checkIds []uint - showErr = false - ) - - app, err := appRepo.GetFirst(appRepo.WithKey(constant.AppOpenresty)) - if err != nil { - return nil, err - } - appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithAppId(app.ID)) - if reflect.DeepEqual(appInstall, model.AppInstall{}) { - res = append(res, response.WebsitePreInstallCheck{ - Name: appInstall.Name, - AppName: app.Name, - Status: buserr.WithDetail(constant.ErrNotInstall, app.Name, nil).Error(), - Version: appInstall.Version, - }) - showErr = true - } else { - checkIds = append(req.InstallIds, appInstall.ID) - } - if len(checkIds) > 0 { - installList, _ := appInstallRepo.ListBy(commonRepo.WithIdsIn(checkIds)) - for _, install := range installList { - if err = syncAppInstallStatus(&install, false); err != nil { - return nil, err - } - res = append(res, response.WebsitePreInstallCheck{ - Name: install.Name, - Status: install.Status, - Version: install.Version, - AppName: install.App.Name, - }) - if install.Status != constant.Running { - showErr = true - } - } - } - if showErr { - return res, nil - } - return nil, nil -} - -func (w WebsiteService) UpdateNginxConfigFile(req request.WebsiteNginxUpdate) error { - website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) - if err != nil { - return err - } - nginxFull, err := getNginxFull(&website) - if err != nil { - return err - } - filePath := nginxFull.SiteConfig.FilePath - if err := files.NewFileOp().WriteFile(filePath, strings.NewReader(req.Content), 0755); err != nil { - return err - } - return nginxCheckAndReload(nginxFull.SiteConfig.OldContent, filePath, nginxFull.Install.ContainerName) -} - -func (w WebsiteService) OpWebsiteLog(req request.WebsiteLogReq) (*response.WebsiteLog, error) { - website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) - if err != nil { - return nil, err - } - nginx, err := getNginxFull(&website) - if err != nil { - return nil, err - } - sitePath := path.Join(nginx.SiteDir, "sites", website.Alias) - res := &response.WebsiteLog{ - Content: "", - } - switch req.Operate { - case constant.GetLog: - switch req.LogType { - case constant.AccessLog: - res.Enable = website.AccessLog - if !website.AccessLog { - return res, nil - } - case constant.ErrorLog: - res.Enable = website.ErrorLog - if !website.ErrorLog { - return res, nil - } - } - filePath := path.Join(sitePath, "log", req.LogType) - lines, end, _, err := files.ReadFileByLine(filePath, req.Page, req.PageSize, false) - if err != nil { - return nil, err - } - res.End = end - res.Path = filePath - res.Content = strings.Join(lines, "\n") - return res, nil - case constant.DisableLog: - key := "access_log" - switch req.LogType { - case constant.AccessLog: - website.AccessLog = false - case constant.ErrorLog: - key = "error_log" - website.ErrorLog = false - } - var nginxParams []dto.NginxParam - nginxParams = append(nginxParams, dto.NginxParam{ - Name: key, - Params: []string{"off"}, - }) - - if err := updateNginxConfig(constant.NginxScopeServer, nginxParams, &website); err != nil { - return nil, err - } - if err := websiteRepo.Save(context.Background(), &website); err != nil { - return nil, err - } - case constant.EnableLog: - key := "access_log" - logPath := path.Join("/www", "sites", website.Alias, "log", req.LogType) - params := []string{logPath} - switch req.LogType { - case constant.AccessLog: - params = append(params, "main") - website.AccessLog = true - case constant.ErrorLog: - key = "error_log" - website.ErrorLog = true - } - if err := updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: key, Params: params}}, &website); err != nil { - return nil, err - } - if err := websiteRepo.Save(context.Background(), &website); err != nil { - return nil, err - } - case constant.DeleteLog: - logPath := path.Join(nginx.Install.GetPath(), "www", "sites", website.Alias, "log", req.LogType) - if err := files.NewFileOp().WriteFile(logPath, strings.NewReader(""), 0755); err != nil { - return nil, err - } - } - return res, nil -} - -func (w WebsiteService) ChangeDefaultServer(id uint) error { - defaultWebsite, _ := websiteRepo.GetFirst(websiteRepo.WithDefaultServer()) - if defaultWebsite.ID > 0 { - params, err := getNginxParamsByKeys(constant.NginxScopeServer, []string{"listen"}, &defaultWebsite) - if err != nil { - return err - } - var changeParams []dto.NginxParam - for _, param := range params { - paramLen := len(param.Params) - var newParam []string - if paramLen > 1 && param.Params[paramLen-1] == components.DefaultServer { - newParam = param.Params[:paramLen-1] - } - changeParams = append(changeParams, dto.NginxParam{ - Name: param.Name, - Params: newParam, - }) - } - if err := updateNginxConfig(constant.NginxScopeServer, changeParams, &defaultWebsite); err != nil { - return err - } - defaultWebsite.DefaultServer = false - if err := websiteRepo.Save(context.Background(), &defaultWebsite); err != nil { - return err - } - } - if id > 0 { - website, err := websiteRepo.GetFirst(commonRepo.WithByID(id)) - if err != nil { - return err - } - params, err := getNginxParamsByKeys(constant.NginxScopeServer, []string{"listen"}, &website) - if err != nil { - return err - } - httpPort, httpsPort, err := getAppInstallPort(constant.AppOpenresty) - if err != nil { - return err - } - - var changeParams []dto.NginxParam - for _, param := range params { - paramLen := len(param.Params) - bind := param.Params[0] - var newParam []string - if bind == strconv.Itoa(httpPort) || bind == strconv.Itoa(httpsPort) || bind == "[::]:"+strconv.Itoa(httpPort) || bind == "[::]:"+strconv.Itoa(httpsPort) { - if param.Params[paramLen-1] == components.DefaultServer { - newParam = param.Params - } else { - newParam = append(param.Params, components.DefaultServer) - } - } - changeParams = append(changeParams, dto.NginxParam{ - Name: param.Name, - Params: newParam, - }) - } - if err := updateNginxConfig(constant.NginxScopeServer, changeParams, &website); err != nil { - return err - } - website.DefaultServer = true - return websiteRepo.Save(context.Background(), &website) - } - return nil -} - -func (w WebsiteService) GetPHPConfig(id uint) (*response.PHPConfig, error) { - website, err := websiteRepo.GetFirst(commonRepo.WithByID(id)) - if err != nil { - return nil, err - } - appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) - if err != nil { - return nil, err - } - phpConfigPath := path.Join(appInstall.GetPath(), "conf", "php.ini") - fileOp := files.NewFileOp() - if !fileOp.Stat(phpConfigPath) { - return nil, buserr.WithMap("ErrFileNotFound", map[string]interface{}{"name": "php.ini"}, nil) - } - params := make(map[string]string) - configFile, err := fileOp.OpenFile(phpConfigPath) - if err != nil { - return nil, err - } - defer configFile.Close() - scanner := bufio.NewScanner(configFile) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if strings.HasPrefix(line, ";") { - continue - } - matches := regexp.MustCompile(`^\s*([a-z_]+)\s*=\s*(.*)$`).FindStringSubmatch(line) - if len(matches) == 3 { - params[matches[1]] = matches[2] - } - } - cfg, err := ini.Load(phpConfigPath) - if err != nil { - return nil, err - } - phpConfig, err := cfg.GetSection("PHP") - if err != nil { - return nil, err - } - disableFunctionStr := phpConfig.Key("disable_functions").Value() - res := &response.PHPConfig{Params: params} - if disableFunctionStr != "" { - disableFunctions := strings.Split(disableFunctionStr, ",") - if len(disableFunctions) > 0 { - res.DisableFunctions = disableFunctions - } - } - uploadMaxSize := phpConfig.Key("upload_max_filesize").Value() - if uploadMaxSize != "" { - res.UploadMaxSize = uploadMaxSize - } - return res, nil -} - -func (w WebsiteService) UpdatePHPConfig(req request.WebsitePHPConfigUpdate) (err error) { - website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) - if err != nil { - return err - } - appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) - if err != nil { - return err - } - phpConfigPath := path.Join(appInstall.GetPath(), "conf", "php.ini") - fileOp := files.NewFileOp() - if !fileOp.Stat(phpConfigPath) { - return buserr.WithMap("ErrFileNotFound", map[string]interface{}{"name": "php.ini"}, nil) - } - configFile, err := fileOp.OpenFile(phpConfigPath) - if err != nil { - return err - } - defer configFile.Close() - - contentBytes, err := fileOp.GetContent(phpConfigPath) - if err != nil { - return err - } - - content := string(contentBytes) - lines := strings.Split(content, "\n") - for i, line := range lines { - if strings.HasPrefix(line, ";") { - continue - } - switch req.Scope { - case "params": - for key, value := range req.Params { - pattern := "^" + regexp.QuoteMeta(key) + "\\s*=\\s*.*$" - if matched, _ := regexp.MatchString(pattern, line); matched { - lines[i] = key + " = " + value - } - } - case "disable_functions": - pattern := "^" + regexp.QuoteMeta("disable_functions") + "\\s*=\\s*.*$" - if matched, _ := regexp.MatchString(pattern, line); matched { - lines[i] = "disable_functions" + " = " + strings.Join(req.DisableFunctions, ",") - break - } - case "upload_max_filesize": - pattern := "^" + regexp.QuoteMeta("post_max_size") + "\\s*=\\s*.*$" - if matched, _ := regexp.MatchString(pattern, line); matched { - lines[i] = "post_max_size" + " = " + req.UploadMaxSize - } - patternUpload := "^" + regexp.QuoteMeta("upload_max_filesize") + "\\s*=\\s*.*$" - if matched, _ := regexp.MatchString(patternUpload, line); matched { - lines[i] = "upload_max_filesize" + " = " + req.UploadMaxSize - } - } - } - updatedContent := strings.Join(lines, "\n") - if err := fileOp.WriteFile(phpConfigPath, strings.NewReader(updatedContent), 0755); err != nil { - return err - } - - appInstallReq := request.AppInstalledOperate{ - InstallId: appInstall.ID, - Operate: constant.Restart, - } - if err = NewIAppInstalledService().Operate(appInstallReq); err != nil { - _ = fileOp.WriteFile(phpConfigPath, strings.NewReader(string(contentBytes)), 0755) - return err - } - - return nil -} - -func (w WebsiteService) UpdatePHPConfigFile(req request.WebsitePHPFileUpdate) error { - website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) - if err != nil { - return err - } - if website.Type != constant.Runtime { - return nil - } - runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(website.RuntimeID)) - if err != nil { - return err - } - if runtime.Resource != constant.ResourceAppstore { - return nil - } - runtimeInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) - if err != nil { - return err - } - configPath := "" - if req.Type == constant.ConfigFPM { - configPath = path.Join(runtimeInstall.GetPath(), "conf", "php-fpm.conf") - } else { - configPath = path.Join(runtimeInstall.GetPath(), "conf", "php.ini") - } - if err := files.NewFileOp().WriteFile(configPath, strings.NewReader(req.Content), 0755); err != nil { - return err - } - if _, err := compose.Restart(runtimeInstall.GetComposePath()); err != nil { - return err - } - return nil -} - -func (w WebsiteService) ChangePHPVersion(req request.WebsitePHPVersionReq) error { - website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) - if err != nil { - return err - } - runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.RuntimeID)) - if err != nil { - return err - } - oldRuntime, err := runtimeRepo.GetFirst(commonRepo.WithByID(website.RuntimeID)) - if err != nil { - return err - } - if runtime.Resource == constant.ResourceLocal || oldRuntime.Resource == constant.ResourceLocal { - return buserr.New("ErrPHPResource") - } - appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) - if err != nil { - return err - } - appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(runtime.AppDetailID)) - if err != nil { - return err - } - - envs := make(map[string]interface{}) - if err = json.Unmarshal([]byte(appInstall.Env), &envs); err != nil { - return err - } - if out, err := compose.Down(appInstall.GetComposePath()); err != nil { - if out != "" { - return errors.New(out) - } - return err - } - - var ( - busErr error - fileOp = files.NewFileOp() - envPath = appInstall.GetEnvPath() - composePath = appInstall.GetComposePath() - confDir = path.Join(appInstall.GetPath(), "conf") - backupConfDir = path.Join(appInstall.GetPath(), "conf_bak") - fpmConfDir = path.Join(confDir, "php-fpm.conf") - phpDir = path.Join(constant.RuntimeDir, runtime.Type, runtime.Name, "php") - oldFmContent, _ = fileOp.GetContent(fpmConfDir) - newComposeByte []byte - ) - envParams := make(map[string]string, len(envs)) - handleMap(envs, envParams) - envParams["IMAGE_NAME"] = runtime.Image - defer func() { - if busErr != nil { - envParams["IMAGE_NAME"] = oldRuntime.Image - _ = env.Write(envParams, envPath) - _ = fileOp.WriteFile(composePath, strings.NewReader(appInstall.DockerCompose), 0775) - if fileOp.Stat(backupConfDir) { - _ = fileOp.DeleteDir(confDir) - _ = fileOp.Rename(backupConfDir, confDir) - } - } - }() - - if busErr = env.Write(envParams, envPath); busErr != nil { - return busErr - } - - newComposeByte, busErr = changeServiceName(appDetail.DockerCompose, appInstall.ServiceName) - if busErr != nil { - return err - } - - if busErr = fileOp.WriteFile(composePath, bytes.NewReader(newComposeByte), 0775); busErr != nil { - return busErr - } - if !req.RetainConfig { - if busErr = fileOp.Rename(confDir, backupConfDir); busErr != nil { - return busErr - } - _ = fileOp.CreateDir(confDir, 0755) - if busErr = fileOp.CopyFile(path.Join(phpDir, "php-fpm.conf"), confDir); busErr != nil { - return busErr - } - if busErr = fileOp.CopyFile(path.Join(phpDir, "php.ini"), confDir); busErr != nil { - _ = fileOp.WriteFile(fpmConfDir, bytes.NewReader(oldFmContent), 0775) - return busErr - } - } - - if out, err := compose.Up(appInstall.GetComposePath()); err != nil { - if out != "" { - busErr = errors.New(out) - return busErr - } - busErr = err - return busErr - } - - _ = fileOp.DeleteDir(backupConfDir) - - appInstall.AppDetailId = runtime.AppDetailID - appInstall.AppId = appDetail.AppId - appInstall.Version = appDetail.Version - appInstall.DockerCompose = string(newComposeByte) - - _ = appInstallRepo.Save(context.Background(), &appInstall) - website.RuntimeID = req.RuntimeID - return websiteRepo.Save(context.Background(), &website) -} - -func (w WebsiteService) UpdateRewriteConfig(req request.NginxRewriteUpdate) error { - website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) - if err != nil { - return err - } - nginxFull, err := getNginxFull(&website) - if err != nil { - return err - } - includePath := fmt.Sprintf("/www/sites/%s/rewrite/%s.conf", website.Alias, website.PrimaryDomain) - absolutePath := path.Join(nginxFull.Install.GetPath(), includePath) - fileOp := files.NewFileOp() - var oldRewriteContent []byte - if !fileOp.Stat(path.Dir(absolutePath)) { - if err := fileOp.CreateDir(path.Dir(absolutePath), 0755); err != nil { - return err - } - } - if !fileOp.Stat(absolutePath) { - if err := fileOp.CreateFile(absolutePath); err != nil { - return err - } - } else { - oldRewriteContent, err = fileOp.GetContent(absolutePath) - if err != nil { - return err - } - } - if err := fileOp.WriteFile(absolutePath, strings.NewReader(req.Content), 0755); err != nil { - return err - } - - if err := updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{includePath}}}, &website); err != nil { - _ = fileOp.WriteFile(absolutePath, bytes.NewReader(oldRewriteContent), 0755) - return err - } - website.Rewrite = req.Name - return websiteRepo.Save(context.Background(), &website) -} - -func (w WebsiteService) GetRewriteConfig(req request.NginxRewriteReq) (*response.NginxRewriteRes, error) { - website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) - if err != nil { - return nil, err - } - var contentByte []byte - if req.Name == "current" { - nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return nil, err - } - rewriteConfPath := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "rewrite", fmt.Sprintf("%s.conf", website.PrimaryDomain)) - fileOp := files.NewFileOp() - if fileOp.Stat(rewriteConfPath) { - contentByte, err = fileOp.GetContent(rewriteConfPath) - if err != nil { - return nil, err - } - } - } else { - rewriteFile := fmt.Sprintf("rewrite/%s.conf", strings.ToLower(req.Name)) - contentByte, err = nginx_conf.Rewrites.ReadFile(rewriteFile) - if err != nil { - return nil, err - } - } - return &response.NginxRewriteRes{ - Content: string(contentByte), - }, err -} - -func (w WebsiteService) UpdateSiteDir(req request.WebsiteUpdateDir) error { - website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) - if err != nil { - return err - } - runDir := req.SiteDir - siteDir := path.Join("/www/sites", website.Alias, "index") - if req.SiteDir != "/" { - siteDir = fmt.Sprintf("%s%s", siteDir, req.SiteDir) - } - if err := updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "root", Params: []string{siteDir}}}, &website); err != nil { - return err - } - website.SiteDir = runDir - return websiteRepo.Save(context.Background(), &website) -} - -func (w WebsiteService) UpdateSitePermission(req request.WebsiteUpdateDirPermission) error { - website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) - if err != nil { - return err - } - nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return err - } - absoluteIndexPath := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "index") - chownCmd := fmt.Sprintf("chown -R %s:%s %s", req.User, req.Group, absoluteIndexPath) - if cmd.HasNoPasswordSudo() { - chownCmd = fmt.Sprintf("sudo %s", chownCmd) - } - if out, err := cmd.ExecWithTimeOut(chownCmd, 10*time.Second); err != nil { - if out != "" { - return errors.New(out) - } - return err - } - website.User = req.User - website.Group = req.Group - return websiteRepo.Save(context.Background(), &website) -} - -func (w WebsiteService) OperateProxy(req request.WebsiteProxyConfig) (err error) { - var ( - website model.Website - params []response.NginxParam - nginxInstall model.AppInstall - par *parser.Parser - oldContent []byte - ) - - website, err = websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) - if err != nil { - return - } - params, err = getNginxParamsByKeys(constant.NginxScopeHttp, []string{"proxy_cache"}, &website) - if err != nil { - return - } - nginxInstall, err = getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return - } - fileOp := files.NewFileOp() - if len(params) == 0 || len(params[0].Params) == 0 { - commonDir := path.Join(nginxInstall.GetPath(), "www", "common", "proxy") - proxyTempPath := path.Join(commonDir, "proxy_temp_dir") - if !fileOp.Stat(proxyTempPath) { - _ = fileOp.CreateDir(proxyTempPath, 0755) - } - proxyCacheDir := path.Join(commonDir, "proxy_temp_dir") - if !fileOp.Stat(proxyCacheDir) { - _ = fileOp.CreateDir(proxyCacheDir, 0755) - } - nginxParams := getNginxParamsFromStaticFile(dto.CACHE, nil) - if err = updateNginxConfig(constant.NginxScopeHttp, nginxParams, &website); err != nil { - return - } - } - includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "proxy") - if !fileOp.Stat(includeDir) { - _ = fileOp.CreateDir(includeDir, 0755) - } - fileName := fmt.Sprintf("%s.conf", req.Name) - includePath := path.Join(includeDir, fileName) - backName := fmt.Sprintf("%s.bak", req.Name) - backPath := path.Join(includeDir, backName) - - if req.Operate == "create" && (fileOp.Stat(includePath) || fileOp.Stat(backPath)) { - err = buserr.New(constant.ErrNameIsExist) - return - } - - defer func() { - if err != nil { - switch req.Operate { - case "create": - _ = fileOp.DeleteFile(includePath) - case "edit": - _ = fileOp.WriteFile(includePath, bytes.NewReader(oldContent), 0755) - } - } - }() - - var config *components.Config - - switch req.Operate { - case "create": - config, err = parser.NewStringParser(string(nginx_conf.Proxy)).Parse() - if err != nil { - return - } - case "edit": - par, err = parser.NewParser(includePath) - if err != nil { - return - } - config, err = par.Parse() - if err != nil { - return - } - oldContent, err = fileOp.GetContent(includePath) - if err != nil { - return - } - case "delete": - _ = fileOp.DeleteFile(includePath) - _ = fileOp.DeleteFile(backPath) - return updateNginxConfig(constant.NginxScopeServer, nil, &website) - case "disable": - _ = fileOp.Rename(includePath, backPath) - return updateNginxConfig(constant.NginxScopeServer, nil, &website) - case "enable": - _ = fileOp.Rename(backPath, includePath) - return updateNginxConfig(constant.NginxScopeServer, nil, &website) - } - - config.FilePath = includePath - directives := config.Directives - location, ok := directives[0].(*components.Location) - if !ok { - err = errors.New("error") - return - } - location.UpdateDirective("proxy_pass", []string{req.ProxyPass}) - location.UpdateDirective("proxy_set_header", []string{"Host", req.ProxyHost}) - location.ChangePath(req.Modifier, req.Match) - if req.Cache { - location.AddCache(req.CacheTime, req.CacheUnit) - } else { - location.RemoveCache() - } - if len(req.Replaces) > 0 { - location.AddSubFilter(req.Replaces) - } else { - location.RemoveSubFilter() - } - if req.SNI { - location.UpdateDirective("proxy_ssl_server_name", []string{"on"}) - } else { - location.UpdateDirective("proxy_ssl_server_name", []string{"off"}) - } - if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { - return buserr.WithErr(constant.ErrUpdateBuWebsite, err) - } - nginxInclude := fmt.Sprintf("/www/sites/%s/proxy/*.conf", website.Alias) - if err = updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website); err != nil { - return - } - return -} - -func (w WebsiteService) GetProxies(id uint) (res []request.WebsiteProxyConfig, err error) { - var ( - website model.Website - nginxInstall model.AppInstall - fileList response.FileInfo - ) - website, err = websiteRepo.GetFirst(commonRepo.WithByID(id)) - if err != nil { - return - } - nginxInstall, err = getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return - } - includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "proxy") - fileOp := files.NewFileOp() - if !fileOp.Stat(includeDir) { - return - } - fileList, err = NewIFileService().GetFileList(request.FileOption{FileOption: files.FileOption{Path: includeDir, Expand: true, Page: 1, PageSize: 100}}) - if len(fileList.Items) == 0 { - return - } - var ( - content []byte - config *components.Config - ) - for _, configFile := range fileList.Items { - proxyConfig := request.WebsiteProxyConfig{ - ID: website.ID, - } - parts := strings.Split(configFile.Name, ".") - proxyConfig.Name = parts[0] - if parts[1] == "conf" { - proxyConfig.Enable = true - } else { - proxyConfig.Enable = false - } - proxyConfig.FilePath = configFile.Path - content, err = fileOp.GetContent(configFile.Path) - if err != nil { - return - } - proxyConfig.Content = string(content) - config, err = parser.NewStringParser(string(content)).Parse() - if err != nil { - return nil, err - } - directives := config.GetDirectives() - - location, ok := directives[0].(*components.Location) - if !ok { - err = errors.New("error") - return - } - proxyConfig.ProxyPass = location.ProxyPass - proxyConfig.Cache = location.Cache - if location.CacheTime > 0 { - proxyConfig.CacheTime = location.CacheTime - proxyConfig.CacheUnit = location.CacheUint - } - proxyConfig.Match = location.Match - proxyConfig.Modifier = location.Modifier - proxyConfig.ProxyHost = location.Host - proxyConfig.Replaces = location.Replaces - for _, directive := range location.Directives { - if directive.GetName() == "proxy_ssl_server_name" { - proxyConfig.SNI = directive.GetParameters()[0] == "on" - } - } - res = append(res, proxyConfig) - } - return -} - -func (w WebsiteService) UpdateProxyFile(req request.NginxProxyUpdate) (err error) { - var ( - website model.Website - nginxFull dto.NginxFull - oldRewriteContent []byte - ) - website, err = websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) - if err != nil { - return err - } - nginxFull, err = getNginxFull(&website) - if err != nil { - return err - } - includePath := fmt.Sprintf("/www/sites/%s/proxy/%s.conf", website.Alias, req.Name) - absolutePath := path.Join(nginxFull.Install.GetPath(), includePath) - fileOp := files.NewFileOp() - oldRewriteContent, err = fileOp.GetContent(absolutePath) - if err != nil { - return err - } - if err = fileOp.WriteFile(absolutePath, strings.NewReader(req.Content), 0755); err != nil { - return err - } - defer func() { - if err != nil { - _ = fileOp.WriteFile(absolutePath, bytes.NewReader(oldRewriteContent), 0755) - } - }() - return updateNginxConfig(constant.NginxScopeServer, nil, &website) -} - -func (w WebsiteService) UpdateAuthBasic(req request.NginxAuthUpdate) (err error) { - var ( - website model.Website - nginxInstall model.AppInstall - params []dto.NginxParam - authContent []byte - authArray []string - ) - website, err = websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) - if err != nil { - return err - } - nginxInstall, err = getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return - } - authPath := fmt.Sprintf("/www/sites/%s/auth_basic/auth.pass", website.Alias) - absoluteAuthPath := path.Join(nginxInstall.GetPath(), authPath) - fileOp := files.NewFileOp() - if !fileOp.Stat(path.Dir(absoluteAuthPath)) { - _ = fileOp.CreateDir(path.Dir(absoluteAuthPath), 0755) - } - if !fileOp.Stat(absoluteAuthPath) { - _ = fileOp.CreateFile(absoluteAuthPath) - } - - params = append(params, dto.NginxParam{Name: "auth_basic", Params: []string{`"Authentication"`}}) - params = append(params, dto.NginxParam{Name: "auth_basic_user_file", Params: []string{authPath}}) - authContent, err = fileOp.GetContent(absoluteAuthPath) - if err != nil { - return - } - if len(authContent) > 0 { - authArray = strings.Split(string(authContent), "\n") - } - switch req.Operate { - case "disable": - return deleteNginxConfig(constant.NginxScopeServer, params, &website) - case "enable": - return updateNginxConfig(constant.NginxScopeServer, params, &website) - case "create": - for _, line := range authArray { - authParams := strings.Split(line, ":") - username := authParams[0] - if username == req.Username { - err = buserr.New(constant.ErrUsernameIsExist) - return - } - } - var passwdHash []byte - passwdHash, err = bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) - if err != nil { - return - } - line := fmt.Sprintf("%s:%s\n", req.Username, passwdHash) - if req.Remark != "" { - line = fmt.Sprintf("%s:%s:%s\n", req.Username, passwdHash, req.Remark) - } - authArray = append(authArray, line) - case "edit": - userExist := false - for index, line := range authArray { - authParams := strings.Split(line, ":") - username := authParams[0] - if username == req.Username { - userExist = true - var passwdHash []byte - passwdHash, err = bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) - if err != nil { - return - } - userPasswd := fmt.Sprintf("%s:%s\n", req.Username, passwdHash) - if req.Remark != "" { - userPasswd = fmt.Sprintf("%s:%s:%s\n", req.Username, passwdHash, req.Remark) - } - authArray[index] = userPasswd - } - } - if !userExist { - err = buserr.New(constant.ErrUsernameIsNotExist) - return - } - case "delete": - deleteIndex := -1 - for index, line := range authArray { - authParams := strings.Split(line, ":") - username := authParams[0] - if username == req.Username { - deleteIndex = index - } - } - if deleteIndex < 0 { - return - } - authArray = append(authArray[:deleteIndex], authArray[deleteIndex+1:]...) - } - - var passFile *os.File - passFile, err = os.Create(absoluteAuthPath) - if err != nil { - return - } - defer passFile.Close() - writer := bufio.NewWriter(passFile) - for _, line := range authArray { - if line == "" { - continue - } - _, err = writer.WriteString(line + "\n") - if err != nil { - return - } - } - err = writer.Flush() - if err != nil { - return - } - authContent, err = fileOp.GetContent(absoluteAuthPath) - if err != nil { - return - } - if len(authContent) == 0 { - if err = deleteNginxConfig(constant.NginxScopeServer, params, &website); err != nil { - return - } - } - return -} - -func (w WebsiteService) GetAuthBasics(req request.NginxAuthReq) (res response.NginxAuthRes, err error) { - var ( - website model.Website - nginxInstall model.AppInstall - authContent []byte - nginxParams []response.NginxParam - ) - website, err = websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) - if err != nil { - return - } - nginxInstall, err = getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return - } - authPath := fmt.Sprintf("/www/sites/%s/auth_basic/auth.pass", website.Alias) - absoluteAuthPath := path.Join(nginxInstall.GetPath(), authPath) - fileOp := files.NewFileOp() - if !fileOp.Stat(absoluteAuthPath) { - return - } - nginxParams, err = getNginxParamsByKeys(constant.NginxScopeServer, []string{"auth_basic"}, &website) - if err != nil { - return - } - res.Enable = len(nginxParams[0].Params) > 0 - authContent, err = fileOp.GetContent(absoluteAuthPath) - authArray := strings.Split(string(authContent), "\n") - for _, line := range authArray { - if line == "" { - continue - } - params := strings.Split(line, ":") - auth := dto.NginxAuth{ - Username: params[0], - } - if len(params) == 3 { - auth.Remark = params[2] - } - res.Items = append(res.Items, auth) - } - return -} - -func (w WebsiteService) UpdateAntiLeech(req request.NginxAntiLeechUpdate) (err error) { - website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) - if err != nil { - return - } - nginxFull, err := getNginxFull(&website) - if err != nil { - return - } - fileOp := files.NewFileOp() - backupContent, err := fileOp.GetContent(nginxFull.SiteConfig.Config.FilePath) - if err != nil { - return - } - block := nginxFull.SiteConfig.Config.FindServers()[0] - locations := block.FindDirectives("location") - for _, location := range locations { - loParams := location.GetParameters() - if len(loParams) > 1 || loParams[0] == "~" { - extendStr := loParams[1] - if strings.HasPrefix(extendStr, `.*\.(`) && strings.HasSuffix(extendStr, `)$`) { - block.RemoveDirective("location", loParams) - } - } - } - if req.Enable { - exts := strings.Split(req.Extends, ",") - newDirective := components.Directive{ - Name: "location", - Parameters: []string{"~", fmt.Sprintf(`.*\.(%s)$`, strings.Join(exts, "|"))}, - } - - newBlock := &components.Block{} - newBlock.Directives = make([]components.IDirective, 0) - if req.Cache { - newBlock.Directives = append(newBlock.Directives, &components.Directive{ - Name: "expires", - Parameters: []string{strconv.Itoa(req.CacheTime) + req.CacheUint}, - }) - } - newBlock.Directives = append(newBlock.Directives, &components.Directive{ - Name: "log_not_found", - Parameters: []string{"off"}, - }) - validDir := &components.Directive{ - Name: "valid_referers", - Parameters: []string{}, - } - if req.NoneRef { - validDir.Parameters = append(validDir.Parameters, "none") - } - if len(req.ServerNames) > 0 { - validDir.Parameters = append(validDir.Parameters, strings.Join(req.ServerNames, " ")) - } - newBlock.Directives = append(newBlock.Directives, validDir) - - ifDir := &components.Directive{ - Name: "if", - Parameters: []string{"($invalid_referer)"}, - } - ifDir.Block = &components.Block{ - Directives: []components.IDirective{ - &components.Directive{ - Name: "return", - Parameters: []string{req.Return}, - }, - &components.Directive{ - Name: "access_log", - Parameters: []string{"off"}, - }, - }, - } - newBlock.Directives = append(newBlock.Directives, ifDir) - newDirective.Block = newBlock - block.Directives = append(block.Directives, &newDirective) - } - - if err = nginx.WriteConfig(nginxFull.SiteConfig.Config, nginx.IndentedStyle); err != nil { - return - } - if err = updateNginxConfig(constant.NginxScopeServer, nil, &website); err != nil { - _ = fileOp.WriteFile(nginxFull.SiteConfig.Config.FilePath, bytes.NewReader(backupContent), 0755) - return - } - return -} - -func (w WebsiteService) GetAntiLeech(id uint) (*response.NginxAntiLeechRes, error) { - website, err := websiteRepo.GetFirst(commonRepo.WithByID(id)) - if err != nil { - return nil, err - } - nginxFull, err := getNginxFull(&website) - if err != nil { - return nil, err - } - res := &response.NginxAntiLeechRes{ - LogEnable: true, - ServerNames: []string{}, - } - block := nginxFull.SiteConfig.Config.FindServers()[0] - locations := block.FindDirectives("location") - for _, location := range locations { - loParams := location.GetParameters() - if len(loParams) > 1 || loParams[0] == "~" { - extendStr := loParams[1] - if strings.HasPrefix(extendStr, `.*\.(`) && strings.HasSuffix(extendStr, `)$`) { - str1 := strings.TrimPrefix(extendStr, `.*\.(`) - str2 := strings.TrimSuffix(str1, ")$") - res.Extends = strings.Join(strings.Split(str2, "|"), ",") - } - } - lDirectives := location.GetBlock().GetDirectives() - for _, lDir := range lDirectives { - if lDir.GetName() == "valid_referers" { - res.Enable = true - params := lDir.GetParameters() - for _, param := range params { - if param == "none" { - res.NoneRef = true - continue - } - if param == "blocked" { - res.Blocked = true - continue - } - if param == "server_names" { - continue - } - res.ServerNames = append(res.ServerNames, param) - } - } - if lDir.GetName() == "if" && lDir.GetParameters()[0] == "($invalid_referer)" { - directives := lDir.GetBlock().GetDirectives() - for _, dir := range directives { - if dir.GetName() == "return" { - res.Return = strings.Join(dir.GetParameters(), " ") - } - if dir.GetName() == "access_log" { - if strings.Join(dir.GetParameters(), "") == "off" { - res.LogEnable = false - } - } - } - } - if lDir.GetName() == "expires" { - res.Cache = true - re := regexp.MustCompile(`^(\d+)(\w+)$`) - matches := re.FindStringSubmatch(lDir.GetParameters()[0]) - if matches == nil { - continue - } - cacheTime, err := strconv.Atoi(matches[1]) - if err != nil { - continue - } - unit := matches[2] - res.CacheUint = unit - res.CacheTime = cacheTime - } - } - } - return res, nil -} - -func (w WebsiteService) OperateRedirect(req request.NginxRedirectReq) (err error) { - var ( - website model.Website - nginxInstall model.AppInstall - oldContent []byte - ) - - website, err = websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) - if err != nil { - return err - } - nginxInstall, err = getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return - } - includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "redirect") - fileOp := files.NewFileOp() - if !fileOp.Stat(includeDir) { - _ = fileOp.CreateDir(includeDir, 0755) - } - fileName := fmt.Sprintf("%s.conf", req.Name) - includePath := path.Join(includeDir, fileName) - backName := fmt.Sprintf("%s.bak", req.Name) - backPath := path.Join(includeDir, backName) - - if req.Operate == "create" && (fileOp.Stat(includePath) || fileOp.Stat(backPath)) { - err = buserr.New(constant.ErrNameIsExist) - return - } - - defer func() { - if err != nil { - switch req.Operate { - case "create": - _ = fileOp.DeleteFile(includePath) - case "edit": - _ = fileOp.WriteFile(includePath, bytes.NewReader(oldContent), 0755) - } - } - }() - - var ( - config *components.Config - oldPar *parser.Parser - ) - - switch req.Operate { - case "create": - config = &components.Config{} - case "edit": - oldPar, err = parser.NewParser(includePath) - if err != nil { - return - } - config, err = oldPar.Parse() - if err != nil { - return - } - oldContent, err = fileOp.GetContent(includePath) - if err != nil { - return - } - case "delete": - _ = fileOp.DeleteFile(includePath) - _ = fileOp.DeleteFile(backPath) - return updateNginxConfig(constant.NginxScopeServer, nil, &website) - case "disable": - _ = fileOp.Rename(includePath, backPath) - return updateNginxConfig(constant.NginxScopeServer, nil, &website) - case "enable": - _ = fileOp.Rename(backPath, includePath) - return updateNginxConfig(constant.NginxScopeServer, nil, &website) - } - - target := req.Target - block := &components.Block{} - - switch req.Type { - case "path": - if req.KeepPath { - target = req.Target + "$1" - } else { - target = req.Target + "?" - } - redirectKey := "permanent" - if req.Redirect == "302" { - redirectKey = "redirect" - } - block = &components.Block{ - Directives: []components.IDirective{ - &components.Directive{ - Name: "rewrite", - Parameters: []string{fmt.Sprintf("^%s(.*)", req.Path), target, redirectKey}, - }, - }, - } - case "domain": - if req.KeepPath { - target = req.Target + "$request_uri" - } - returnBlock := &components.Block{ - Directives: []components.IDirective{ - &components.Directive{ - Name: "return", - Parameters: []string{req.Redirect, target}, - }, - }, - } - for _, domain := range req.Domains { - block.Directives = append(block.Directives, &components.Directive{ - Name: "if", - Parameters: []string{"($host", "~", fmt.Sprintf("'^%s')", domain)}, - Block: returnBlock, - }) - } - case "404": - if req.RedirectRoot { - target = "/" - } - block = &components.Block{ - Directives: []components.IDirective{ - &components.Directive{ - Name: "error_page", - Parameters: []string{"404", "=", "@notfound"}, - }, - &components.Directive{ - Name: "location", - Parameters: []string{"@notfound"}, - Block: &components.Block{ - Directives: []components.IDirective{ - &components.Directive{ - Name: "return", - Parameters: []string{req.Redirect, target}, - }, - }, - }, - }, - }, - } - } - config.FilePath = includePath - config.Block = block - - if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { - return buserr.WithErr(constant.ErrUpdateBuWebsite, err) - } - - nginxInclude := fmt.Sprintf("/www/sites/%s/redirect/*.conf", website.Alias) - if err = updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website); err != nil { - return - } - return -} - -func (w WebsiteService) GetRedirect(id uint) (res []response.NginxRedirectConfig, err error) { - var ( - website model.Website - nginxInstall model.AppInstall - fileList response.FileInfo - ) - website, err = websiteRepo.GetFirst(commonRepo.WithByID(id)) - if err != nil { - return - } - nginxInstall, err = getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return - } - includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "redirect") - fileOp := files.NewFileOp() - if !fileOp.Stat(includeDir) { - return - } - fileList, err = NewIFileService().GetFileList(request.FileOption{FileOption: files.FileOption{Path: includeDir, Expand: true, Page: 1, PageSize: 100}}) - if len(fileList.Items) == 0 { - return - } - var ( - content []byte - config *components.Config - ) - for _, configFile := range fileList.Items { - redirectConfig := response.NginxRedirectConfig{ - WebsiteID: website.ID, - } - parts := strings.Split(configFile.Name, ".") - redirectConfig.Name = parts[0] - if parts[1] == "conf" { - redirectConfig.Enable = true - } else { - redirectConfig.Enable = false - } - redirectConfig.FilePath = configFile.Path - content, err = fileOp.GetContent(configFile.Path) - if err != nil { - return - } - redirectConfig.Content = string(content) - config, err = parser.NewStringParser(string(content)).Parse() - if err != nil { - return - } - - dirs := config.GetDirectives() - if len(dirs) > 0 { - firstName := dirs[0].GetName() - switch firstName { - case "if": - for _, ifDir := range dirs { - params := ifDir.GetParameters() - if len(params) > 2 && params[0] == "($host" { - domain := strings.Trim(strings.Trim(params[2], "'"), "^") - redirectConfig.Domains = append(redirectConfig.Domains, domain) - if len(redirectConfig.Domains) > 1 { - continue - } - redirectConfig.Type = "domain" - } - childDirs := ifDir.GetBlock().GetDirectives() - for _, dir := range childDirs { - if dir.GetName() == "return" { - dirParams := dir.GetParameters() - if len(dirParams) > 1 { - redirectConfig.Redirect = dirParams[0] - if strings.HasSuffix(dirParams[1], "$request_uri") { - redirectConfig.KeepPath = true - redirectConfig.Target = strings.TrimSuffix(dirParams[1], "$request_uri") - } else { - redirectConfig.KeepPath = false - redirectConfig.Target = dirParams[1] - } - } - } - } - } - case "rewrite": - redirectConfig.Type = "path" - for _, pathDir := range dirs { - if pathDir.GetName() == "rewrite" { - params := pathDir.GetParameters() - if len(params) > 2 { - redirectConfig.Path = strings.Trim(strings.Trim(params[0], "^"), "(.*)") - if strings.HasSuffix(params[1], "$1") { - redirectConfig.KeepPath = true - redirectConfig.Target = strings.TrimSuffix(params[1], "$1") - } else { - redirectConfig.KeepPath = false - redirectConfig.Target = strings.TrimSuffix(params[1], "?") - } - if params[2] == "permanent" { - redirectConfig.Redirect = "301" - } else { - redirectConfig.Redirect = "302" - } - } - } - } - case "error_page": - redirectConfig.Type = "404" - for _, errDir := range dirs { - if errDir.GetName() == "location" { - childDirs := errDir.GetBlock().GetDirectives() - for _, dir := range childDirs { - if dir.GetName() == "return" { - dirParams := dir.GetParameters() - if len(dirParams) > 1 { - redirectConfig.Redirect = dirParams[0] - if strings.HasSuffix(dirParams[1], "$request_uri") { - redirectConfig.KeepPath = true - redirectConfig.Target = strings.TrimSuffix(dirParams[1], "$request_uri") - redirectConfig.RedirectRoot = false - } else { - redirectConfig.KeepPath = false - redirectConfig.Target = dirParams[1] - redirectConfig.RedirectRoot = redirectConfig.Target == "/" - } - } - } - } - } - } - } - } - res = append(res, redirectConfig) - } - return -} - -func (w WebsiteService) UpdateRedirectFile(req request.NginxRedirectUpdate) (err error) { - var ( - website model.Website - nginxFull dto.NginxFull - oldRewriteContent []byte - ) - website, err = websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) - if err != nil { - return err - } - nginxFull, err = getNginxFull(&website) - if err != nil { - return err - } - includePath := fmt.Sprintf("/www/sites/%s/redirect/%s.conf", website.Alias, req.Name) - absolutePath := path.Join(nginxFull.Install.GetPath(), includePath) - fileOp := files.NewFileOp() - oldRewriteContent, err = fileOp.GetContent(absolutePath) - if err != nil { - return err - } - if err = fileOp.WriteFile(absolutePath, strings.NewReader(req.Content), 0755); err != nil { - return err - } - defer func() { - if err != nil { - _ = fileOp.WriteFile(absolutePath, bytes.NewReader(oldRewriteContent), 0755) - } - }() - return updateNginxConfig(constant.NginxScopeServer, nil, &website) -} - -func (w WebsiteService) LoadWebsiteDirConfig(req request.WebsiteCommonReq) (*response.WebsiteDirConfig, error) { - website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) - if err != nil { - return nil, err - } - res := &response.WebsiteDirConfig{} - nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return nil, err - } - absoluteIndexPath := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "index") - var appFs = afero.NewOsFs() - info, err := appFs.Stat(absoluteIndexPath) - if err != nil { - return nil, err - } - res.User = strconv.FormatUint(uint64(info.Sys().(*syscall.Stat_t).Uid), 10) - res.UserGroup = strconv.FormatUint(uint64(info.Sys().(*syscall.Stat_t).Gid), 10) - - indexFiles, err := os.ReadDir(absoluteIndexPath) - if err != nil { - return nil, err - } - res.Dirs = []string{"/"} - for _, file := range indexFiles { - if !file.IsDir() { - continue - } - res.Dirs = append(res.Dirs, fmt.Sprintf("/%s", file.Name())) - fileInfo, _ := file.Info() - if fileInfo.Sys().(*syscall.Stat_t).Uid != 1000 || fileInfo.Sys().(*syscall.Stat_t).Gid != 1000 { - res.Msg = i18n.GetMsgByKey("ErrPathPermission") - } - childFiles, _ := os.ReadDir(absoluteIndexPath + "/" + file.Name()) - for _, childFile := range childFiles { - if !childFile.IsDir() { - continue - } - childInfo, _ := childFile.Info() - if childInfo.Sys().(*syscall.Stat_t).Uid != 1000 || childInfo.Sys().(*syscall.Stat_t).Gid != 1000 { - res.Msg = i18n.GetMsgByKey("ErrPathPermission") - } - res.Dirs = append(res.Dirs, fmt.Sprintf("/%s/%s", file.Name(), childFile.Name())) - } - } - - return res, nil -} - -func (w WebsiteService) GetDefaultHtml(resourceType string) (*response.WebsiteHtmlRes, error) { - nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return nil, err - } - rootPath := path.Join(nginxInstall.GetPath(), "root") - fileOp := files.NewFileOp() - defaultPath := path.Join(rootPath, "default") - if !fileOp.Stat(defaultPath) { - _ = fileOp.CreateDir(defaultPath, 0755) - } - - res := &response.WebsiteHtmlRes{} - - switch resourceType { - case "404": - resourcePath := path.Join(defaultPath, "404.html") - if content, _ := getResourceContent(fileOp, resourcePath); content != "" { - res.Content = content - return res, nil - } - res.Content = string(nginx_conf.NotFoundHTML) - return res, nil - case "php": - resourcePath := path.Join(defaultPath, "index.php") - if content, _ := getResourceContent(fileOp, resourcePath); content != "" { - res.Content = content - return res, nil - } - res.Content = string(nginx_conf.IndexPHP) - return res, nil - case "index": - resourcePath := path.Join(defaultPath, "index.html") - if content, _ := getResourceContent(fileOp, resourcePath); content != "" { - res.Content = content - return res, nil - } - res.Content = string(nginx_conf.Index) - return res, nil - case "domain404": - resourcePath := path.Join(rootPath, "404.html") - if content, _ := getResourceContent(fileOp, resourcePath); content != "" { - res.Content = content - return res, nil - } - res.Content = string(nginx_conf.DomainNotFoundHTML) - return res, nil - case "stop": - resourcePath := path.Join(rootPath, "stop", "index.html") - if content, _ := getResourceContent(fileOp, resourcePath); content != "" { - res.Content = content - return res, nil - } - res.Content = string(nginx_conf.StopHTML) - return res, nil - } - return res, nil -} - -func (w WebsiteService) UpdateDefaultHtml(req request.WebsiteHtmlUpdate) error { - nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return err - } - rootPath := path.Join(nginxInstall.GetPath(), "root") - fileOp := files.NewFileOp() - defaultPath := path.Join(rootPath, "default") - if !fileOp.Stat(defaultPath) { - _ = fileOp.CreateDir(defaultPath, 0755) - } - var resourcePath string - switch req.Type { - case "404": - resourcePath = path.Join(defaultPath, "404.html") - case "php": - resourcePath = path.Join(defaultPath, "index.php") - case "index": - resourcePath = path.Join(defaultPath, "index.html") - case "domain404": - resourcePath = path.Join(rootPath, "404.html") - case "stop": - resourcePath = path.Join(rootPath, "stop", "index.html") - default: - return nil - } - return fileOp.SaveFile(resourcePath, req.Content, 0644) -} diff --git a/backend/app/service/website_acme_account.go b/backend/app/service/website_acme_account.go deleted file mode 100644 index a433e95a2..000000000 --- a/backend/app/service/website_acme_account.go +++ /dev/null @@ -1,78 +0,0 @@ -package service - -import ( - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/app/dto/response" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/ssl" -) - -type WebsiteAcmeAccountService struct { -} - -type IWebsiteAcmeAccountService interface { - Page(search dto.PageInfo) (int64, []response.WebsiteAcmeAccountDTO, error) - Create(create request.WebsiteAcmeAccountCreate) (*response.WebsiteAcmeAccountDTO, error) - Delete(id uint) error -} - -func NewIWebsiteAcmeAccountService() IWebsiteAcmeAccountService { - return &WebsiteAcmeAccountService{} -} - -func (w WebsiteAcmeAccountService) Page(search dto.PageInfo) (int64, []response.WebsiteAcmeAccountDTO, error) { - total, accounts, err := websiteAcmeRepo.Page(search.Page, search.PageSize, commonRepo.WithOrderBy("created_at desc")) - var accountDTOs []response.WebsiteAcmeAccountDTO - for _, account := range accounts { - accountDTOs = append(accountDTOs, response.WebsiteAcmeAccountDTO{ - WebsiteAcmeAccount: account, - }) - } - return total, accountDTOs, err -} - -func (w WebsiteAcmeAccountService) Create(create request.WebsiteAcmeAccountCreate) (*response.WebsiteAcmeAccountDTO, error) { - exist, _ := websiteAcmeRepo.GetFirst(websiteAcmeRepo.WithEmail(create.Email), websiteAcmeRepo.WithType(create.Type)) - if exist != nil { - return nil, buserr.New(constant.ErrEmailIsExist) - } - acmeAccount := &model.WebsiteAcmeAccount{ - Email: create.Email, - Type: create.Type, - KeyType: create.KeyType, - } - - if create.Type == "google" { - if create.EabKid == "" || create.EabHmacKey == "" { - return nil, buserr.New(constant.ErrEabKidOrEabHmacKeyCannotBlank) - } - acmeAccount.EabKid = create.EabKid - acmeAccount.EabHmacKey = create.EabHmacKey - } - - client, err := ssl.NewAcmeClient(acmeAccount) - if err != nil { - return nil, err - } - privateKey, err := ssl.GetPrivateKey(client.User.GetPrivateKey(), ssl.KeyType(create.KeyType)) - if err != nil { - return nil, err - } - acmeAccount.PrivateKey = string(privateKey) - acmeAccount.URL = client.User.Registration.URI - - if err := websiteAcmeRepo.Create(*acmeAccount); err != nil { - return nil, err - } - return &response.WebsiteAcmeAccountDTO{WebsiteAcmeAccount: *acmeAccount}, nil -} - -func (w WebsiteAcmeAccountService) Delete(id uint) error { - if ssls, _ := websiteSSLRepo.List(websiteSSLRepo.WithByAcmeAccountId(id)); len(ssls) > 0 { - return buserr.New(constant.ErrAccountCannotDelete) - } - return websiteAcmeRepo.DeleteBy(commonRepo.WithByID(id)) -} diff --git a/backend/app/service/website_ca.go b/backend/app/service/website_ca.go deleted file mode 100644 index a93b6ad34..000000000 --- a/backend/app/service/website_ca.go +++ /dev/null @@ -1,446 +0,0 @@ -package service - -import ( - "bytes" - "context" - "crypto/ecdsa" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "fmt" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/app/dto/response" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/i18n" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/1Panel-dev/1Panel/backend/utils/ssl" - "github.com/go-acme/lego/v4/certcrypto" - "log" - "math/big" - "net" - "os" - "path" - "strings" - "time" -) - -type WebsiteCAService struct { -} - -type IWebsiteCAService interface { - Page(search request.WebsiteCASearch) (int64, []response.WebsiteCADTO, error) - Create(create request.WebsiteCACreate) (*request.WebsiteCACreate, error) - GetCA(id uint) (*response.WebsiteCADTO, error) - Delete(id uint) error - ObtainSSL(req request.WebsiteCAObtain) (*model.WebsiteSSL, error) - DownloadFile(id uint) (*os.File, error) -} - -func NewIWebsiteCAService() IWebsiteCAService { - return &WebsiteCAService{} -} - -func (w WebsiteCAService) Page(search request.WebsiteCASearch) (int64, []response.WebsiteCADTO, error) { - total, cas, err := websiteCARepo.Page(search.Page, search.PageSize, commonRepo.WithOrderBy("created_at desc")) - if err != nil { - return 0, nil, err - } - var caDTOs []response.WebsiteCADTO - for _, ca := range cas { - caDTOs = append(caDTOs, response.WebsiteCADTO{ - WebsiteCA: ca, - }) - } - return total, caDTOs, err -} - -func (w WebsiteCAService) Create(create request.WebsiteCACreate) (*request.WebsiteCACreate, error) { - if exist, _ := websiteCARepo.GetFirst(commonRepo.WithByName(create.Name)); exist.ID > 0 { - return nil, buserr.New(constant.ErrNameIsExist) - } - - ca := &model.WebsiteCA{ - Name: create.Name, - KeyType: create.KeyType, - } - - pkixName := pkix.Name{ - CommonName: create.CommonName, - Country: []string{create.Country}, - Organization: []string{create.Organization}, - OrganizationalUnit: []string{create.OrganizationUint}, - } - if create.Province != "" { - pkixName.Province = []string{create.Province} - } - if create.City != "" { - pkixName.Locality = []string{create.City} - } - - rootCA := &x509.Certificate{ - SerialNumber: big.NewInt(time.Now().Unix() + 1), - Subject: pkixName, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(10, 0, 0), - BasicConstraintsValid: true, - IsCA: true, - MaxPathLen: 1, - MaxPathLenZero: false, - KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, - } - - interPrivateKey, interPublicKey, privateBytes, err := createPrivateKey(create.KeyType) - if err != nil { - return nil, err - } - ca.PrivateKey = string(privateBytes) - - rootDer, err := x509.CreateCertificate(rand.Reader, rootCA, rootCA, interPublicKey, interPrivateKey) - if err != nil { - return nil, err - } - rootCert, err := x509.ParseCertificate(rootDer) - if err != nil { - return nil, err - } - certBlock := &pem.Block{ - Type: "CERTIFICATE", - Bytes: rootCert.Raw, - } - pemData := pem.EncodeToMemory(certBlock) - ca.CSR = string(pemData) - - if err := websiteCARepo.Create(context.Background(), ca); err != nil { - return nil, err - } - return &create, nil -} - -func (w WebsiteCAService) GetCA(id uint) (*response.WebsiteCADTO, error) { - res := &response.WebsiteCADTO{} - ca, err := websiteCARepo.GetFirst(commonRepo.WithByID(id)) - if err != nil { - return nil, err - } - res.WebsiteCA = ca - certBlock, _ := pem.Decode([]byte(ca.CSR)) - if certBlock == nil { - return nil, buserr.New("ErrSSLCertificateFormat") - } - cert, err := x509.ParseCertificate(certBlock.Bytes) - if err != nil { - return nil, err - } - res.CommonName = cert.Issuer.CommonName - res.Organization = strings.Join(cert.Issuer.Organization, ",") - res.Country = strings.Join(cert.Issuer.Country, ",") - res.Province = strings.Join(cert.Issuer.Province, ",") - res.City = strings.Join(cert.Issuer.Locality, ",") - res.OrganizationUint = strings.Join(cert.Issuer.OrganizationalUnit, ",") - - return res, nil -} - -func (w WebsiteCAService) Delete(id uint) error { - ssls, _ := websiteSSLRepo.List(websiteSSLRepo.WithByCAID(id)) - if len(ssls) > 0 { - return buserr.New("ErrDeleteCAWithSSL") - } - exist, err := websiteCARepo.GetFirst(commonRepo.WithByID(id)) - if err != nil { - return err - } - if exist.Name == "1Panel" { - return buserr.New("ErrDefaultCA") - } - return websiteCARepo.DeleteBy(commonRepo.WithByID(id)) -} - -func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.WebsiteSSL, error) { - var ( - domains []string - ips []net.IP - websiteSSL = &model.WebsiteSSL{} - err error - ca model.WebsiteCA - ) - if req.Renew { - websiteSSL, err = websiteSSLRepo.GetFirst(commonRepo.WithByID(req.SSLID)) - if err != nil { - return nil, err - } - ca, err = websiteCARepo.GetFirst(commonRepo.WithByID(websiteSSL.CaID)) - if err != nil { - return nil, err - } - existDomains := []string{websiteSSL.PrimaryDomain} - if websiteSSL.Domains != "" { - existDomains = append(existDomains, strings.Split(websiteSSL.Domains, ",")...) - } - for _, domain := range existDomains { - if ipAddress := net.ParseIP(domain); ipAddress == nil { - domains = append(domains, domain) - } else { - ips = append(ips, ipAddress) - } - } - } else { - ca, err = websiteCARepo.GetFirst(commonRepo.WithByID(req.ID)) - if err != nil { - return nil, err - } - websiteSSL = &model.WebsiteSSL{ - Provider: constant.SelfSigned, - KeyType: req.KeyType, - PushDir: req.PushDir, - CaID: ca.ID, - AutoRenew: req.AutoRenew, - Description: req.Description, - ExecShell: req.ExecShell, - } - if req.ExecShell { - websiteSSL.Shell = req.Shell - } - if req.PushDir { - if !files.NewFileOp().Stat(req.Dir) { - return nil, buserr.New(constant.ErrLinkPathNotFound) - } - websiteSSL.Dir = req.Dir - } - if req.Domains != "" { - domainArray := strings.Split(req.Domains, "\n") - for _, domain := range domainArray { - if !common.IsValidDomain(domain) { - err = buserr.WithName("ErrDomainFormat", domain) - return nil, err - } else { - if ipAddress := net.ParseIP(domain); ipAddress == nil { - domains = append(domains, domain) - } else { - ips = append(ips, ipAddress) - } - } - } - if len(domains) > 0 { - websiteSSL.PrimaryDomain = domains[0] - websiteSSL.Domains = strings.Join(domains[1:], ",") - } - ipStrings := make([]string, len(ips)) - for i, ip := range ips { - ipStrings[i] = ip.String() - } - if websiteSSL.PrimaryDomain == "" && len(ips) > 0 { - websiteSSL.PrimaryDomain = ipStrings[0] - ipStrings = ipStrings[1:] - } - if len(ipStrings) > 0 { - if websiteSSL.Domains != "" { - websiteSSL.Domains += "," - } - websiteSSL.Domains += strings.Join(ipStrings, ",") - } - - } - } - - rootCertBlock, _ := pem.Decode([]byte(ca.CSR)) - if rootCertBlock == nil { - return nil, buserr.New("ErrSSLCertificateFormat") - } - rootCsr, err := x509.ParseCertificate(rootCertBlock.Bytes) - if err != nil { - return nil, err - } - rootPrivateKeyBlock, _ := pem.Decode([]byte(ca.PrivateKey)) - if rootPrivateKeyBlock == nil { - return nil, buserr.New("ErrSSLCertificateFormat") - } - - var rootPrivateKey any - if ssl.KeyType(ca.KeyType) == certcrypto.EC256 || ssl.KeyType(ca.KeyType) == certcrypto.EC384 { - rootPrivateKey, err = x509.ParseECPrivateKey(rootPrivateKeyBlock.Bytes) - if err != nil { - return nil, err - } - } else { - rootPrivateKey, err = x509.ParsePKCS1PrivateKey(rootPrivateKeyBlock.Bytes) - if err != nil { - return nil, err - } - } - interPrivateKey, interPublicKey, _, err := createPrivateKey(websiteSSL.KeyType) - if err != nil { - return nil, err - } - notAfter := time.Now() - if req.Unit == "year" { - notAfter = notAfter.AddDate(req.Time, 0, 0) - } else { - notAfter = notAfter.AddDate(0, 0, req.Time) - } - interCsr := &x509.Certificate{ - SerialNumber: big.NewInt(time.Now().Unix() + 2), - Subject: rootCsr.Subject, - NotBefore: time.Now(), - NotAfter: notAfter, - BasicConstraintsValid: true, - IsCA: true, - MaxPathLen: 0, - MaxPathLenZero: true, - KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, - } - interDer, err := x509.CreateCertificate(rand.Reader, interCsr, rootCsr, interPublicKey, rootPrivateKey) - if err != nil { - return nil, err - } - interCert, err := x509.ParseCertificate(interDer) - if err != nil { - return nil, err - } - interCertBlock := &pem.Block{ - Type: "CERTIFICATE", - Bytes: interCert.Raw, - } - _, publicKey, privateKeyBytes, err := createPrivateKey(websiteSSL.KeyType) - if err != nil { - return nil, err - } - commonName := "" - if len(domains) > 0 { - commonName = domains[0] - } - if len(ips) > 0 { - commonName = ips[0].String() - } - subject := rootCsr.Subject - subject.CommonName = commonName - csr := &x509.Certificate{ - SerialNumber: big.NewInt(time.Now().Unix() + 3), - Subject: subject, - NotBefore: time.Now(), - NotAfter: notAfter, - BasicConstraintsValid: true, - IsCA: false, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - DNSNames: domains, - IPAddresses: ips, - } - - der, err := x509.CreateCertificate(rand.Reader, csr, interCert, publicKey, interPrivateKey) - if err != nil { - return nil, err - } - cert, err := x509.ParseCertificate(der) - if err != nil { - return nil, err - } - - certBlock := &pem.Block{ - Type: "CERTIFICATE", - Bytes: cert.Raw, - } - websiteSSL.Pem = string(pem.EncodeToMemory(certBlock)) + string(pem.EncodeToMemory(rootCertBlock)) + string(pem.EncodeToMemory(interCertBlock)) - websiteSSL.PrivateKey = string(privateKeyBytes) - websiteSSL.ExpireDate = cert.NotAfter - websiteSSL.StartDate = cert.NotBefore - websiteSSL.Type = cert.Issuer.CommonName - websiteSSL.Organization = rootCsr.Subject.Organization[0] - websiteSSL.Status = constant.SSLReady - - if req.Renew { - if err := websiteSSLRepo.Save(websiteSSL); err != nil { - return nil, err - } - } else { - if err := websiteSSLRepo.Create(context.Background(), websiteSSL); err != nil { - return nil, err - } - } - - logFile, _ := os.OpenFile(path.Join(constant.SSLLogDir, fmt.Sprintf("%s-ssl-%d.log", websiteSSL.PrimaryDomain, websiteSSL.ID)), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666) - defer logFile.Close() - logger := log.New(logFile, "", log.LstdFlags) - logger.Println(i18n.GetMsgWithMap("ApplySSLSuccess", map[string]interface{}{"domain": strings.Join(domains, ",")})) - saveCertificateFile(websiteSSL, logger) - if websiteSSL.ExecShell { - workDir := constant.DataDir - if websiteSSL.PushDir { - workDir = websiteSSL.Dir - } - logger.Println(i18n.GetMsgByKey("ExecShellStart")) - if err = cmd.ExecShellWithTimeOut(websiteSSL.Shell, workDir, logger, 30*time.Minute); err != nil { - logger.Println(i18n.GetMsgWithMap("ErrExecShell", map[string]interface{}{"err": err.Error()})) - } else { - logger.Println(i18n.GetMsgByKey("ExecShellSuccess")) - } - } - return websiteSSL, nil -} - -func createPrivateKey(keyType string) (privateKey any, publicKey any, privateKeyBytes []byte, err error) { - privateKey, err = certcrypto.GeneratePrivateKey(ssl.KeyType(keyType)) - if err != nil { - return - } - var ( - caPrivateKeyPEM = new(bytes.Buffer) - ) - if ssl.KeyType(keyType) == certcrypto.EC256 || ssl.KeyType(keyType) == certcrypto.EC384 { - publicKey = &privateKey.(*ecdsa.PrivateKey).PublicKey - publicKey = publicKey.(*ecdsa.PublicKey) - block := &pem.Block{ - Type: "EC PRIVATE KEY", - } - privateBytes, sErr := x509.MarshalECPrivateKey(privateKey.(*ecdsa.PrivateKey)) - if sErr != nil { - err = sErr - return - } - block.Bytes = privateBytes - _ = pem.Encode(caPrivateKeyPEM, block) - } else { - publicKey = &privateKey.(*rsa.PrivateKey).PublicKey - _ = pem.Encode(caPrivateKeyPEM, &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(privateKey.(*rsa.PrivateKey)), - }) - } - privateKeyBytes = caPrivateKeyPEM.Bytes() - return -} - -func (w WebsiteCAService) DownloadFile(id uint) (*os.File, error) { - ca, err := websiteCARepo.GetFirst(commonRepo.WithByID(id)) - if err != nil { - return nil, err - } - fileOp := files.NewFileOp() - dir := path.Join(global.CONF.System.BaseDir, "1panel/tmp/ssl", ca.Name) - if fileOp.Stat(dir) { - if err = fileOp.DeleteDir(dir); err != nil { - return nil, err - } - } - if err = fileOp.CreateDir(dir, 0666); err != nil { - return nil, err - } - if err = fileOp.WriteFile(path.Join(dir, "ca.csr"), strings.NewReader(ca.CSR), 0644); err != nil { - return nil, err - } - if err = fileOp.WriteFile(path.Join(dir, "private.key"), strings.NewReader(ca.PrivateKey), 0644); err != nil { - return nil, err - } - fileName := ca.Name + ".zip" - if err = fileOp.Compress([]string{path.Join(dir, "ca.csr"), path.Join(dir, "private.key")}, dir, fileName, files.SdkZip, ""); err != nil { - return nil, err - } - return os.Open(path.Join(dir, fileName)) -} diff --git a/backend/app/service/website_dns_account.go b/backend/app/service/website_dns_account.go deleted file mode 100644 index 152853661..000000000 --- a/backend/app/service/website_dns_account.go +++ /dev/null @@ -1,93 +0,0 @@ -package service - -import ( - "encoding/json" - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/app/dto/response" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" -) - -type WebsiteDnsAccountService struct { -} - -type IWebsiteDnsAccountService interface { - Page(search dto.PageInfo) (int64, []response.WebsiteDnsAccountDTO, error) - Create(create request.WebsiteDnsAccountCreate) (request.WebsiteDnsAccountCreate, error) - Update(update request.WebsiteDnsAccountUpdate) (request.WebsiteDnsAccountUpdate, error) - Delete(id uint) error -} - -func NewIWebsiteDnsAccountService() IWebsiteDnsAccountService { - return &WebsiteDnsAccountService{} -} - -func (w WebsiteDnsAccountService) Page(search dto.PageInfo) (int64, []response.WebsiteDnsAccountDTO, error) { - total, accounts, err := websiteDnsRepo.Page(search.Page, search.PageSize, commonRepo.WithOrderBy("created_at desc")) - var accountDTOs []response.WebsiteDnsAccountDTO - for _, account := range accounts { - auth := make(map[string]string) - _ = json.Unmarshal([]byte(account.Authorization), &auth) - accountDTOs = append(accountDTOs, response.WebsiteDnsAccountDTO{ - WebsiteDnsAccount: account, - Authorization: auth, - }) - } - return total, accountDTOs, err -} - -func (w WebsiteDnsAccountService) Create(create request.WebsiteDnsAccountCreate) (request.WebsiteDnsAccountCreate, error) { - exist, _ := websiteDnsRepo.GetFirst(commonRepo.WithByName(create.Name)) - if exist != nil { - return request.WebsiteDnsAccountCreate{}, buserr.New(constant.ErrNameIsExist) - } - - authorization, err := json.Marshal(create.Authorization) - if err != nil { - return request.WebsiteDnsAccountCreate{}, err - } - - if err := websiteDnsRepo.Create(model.WebsiteDnsAccount{ - Name: create.Name, - Type: create.Type, - Authorization: string(authorization), - }); err != nil { - return request.WebsiteDnsAccountCreate{}, err - } - - return create, nil -} - -func (w WebsiteDnsAccountService) Update(update request.WebsiteDnsAccountUpdate) (request.WebsiteDnsAccountUpdate, error) { - authorization, err := json.Marshal(update.Authorization) - if err != nil { - return request.WebsiteDnsAccountUpdate{}, err - } - exists, _ := websiteDnsRepo.List(commonRepo.WithByName(update.Name)) - for _, exist := range exists { - if exist.ID != update.ID { - return request.WebsiteDnsAccountUpdate{}, buserr.New(constant.ErrNameIsExist) - } - } - if err := websiteDnsRepo.Save(model.WebsiteDnsAccount{ - BaseModel: model.BaseModel{ - ID: update.ID, - }, - Name: update.Name, - Type: update.Type, - Authorization: string(authorization), - }); err != nil { - return request.WebsiteDnsAccountUpdate{}, err - } - - return update, nil -} - -func (w WebsiteDnsAccountService) Delete(id uint) error { - if ssls, _ := websiteSSLRepo.List(websiteSSLRepo.WithByDnsAccountId(id)); len(ssls) > 0 { - return buserr.New(constant.ErrAccountCannotDelete) - } - return websiteDnsRepo.DeleteBy(commonRepo.WithByID(id)) -} diff --git a/backend/app/service/website_ssl.go b/backend/app/service/website_ssl.go deleted file mode 100644 index 761903242..000000000 --- a/backend/app/service/website_ssl.go +++ /dev/null @@ -1,624 +0,0 @@ -package service - -import ( - "context" - "crypto" - "crypto/x509" - "encoding/pem" - "fmt" - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/app/dto/response" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/app/repo" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/i18n" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/1Panel-dev/1Panel/backend/utils/ssl" - "github.com/go-acme/lego/v4/certcrypto" - legoLogger "github.com/go-acme/lego/v4/log" - "github.com/jinzhu/gorm" - "log" - "os" - "path" - "strconv" - "strings" - "time" -) - -type WebsiteSSLService struct { -} - -type IWebsiteSSLService interface { - Page(search request.WebsiteSSLSearch) (int64, []response.WebsiteSSLDTO, error) - GetSSL(id uint) (*response.WebsiteSSLDTO, error) - Search(req request.WebsiteSSLSearch) ([]response.WebsiteSSLDTO, error) - Create(create request.WebsiteSSLCreate) (request.WebsiteSSLCreate, error) - GetDNSResolve(req request.WebsiteDNSReq) ([]response.WebsiteDNSRes, error) - GetWebsiteSSL(websiteId uint) (response.WebsiteSSLDTO, error) - Delete(ids []uint) error - Update(update request.WebsiteSSLUpdate) error - Upload(req request.WebsiteSSLUpload) error - ObtainSSL(apply request.WebsiteSSLApply) error - SyncForRestart() error - DownloadFile(id uint) (*os.File, error) -} - -func NewIWebsiteSSLService() IWebsiteSSLService { - return &WebsiteSSLService{} -} - -func (w WebsiteSSLService) Page(search request.WebsiteSSLSearch) (int64, []response.WebsiteSSLDTO, error) { - var ( - result []response.WebsiteSSLDTO - ) - total, sslList, err := websiteSSLRepo.Page(search.Page, search.PageSize, commonRepo.WithOrderBy("created_at desc")) - if err != nil { - return 0, nil, err - } - for _, model := range sslList { - result = append(result, response.WebsiteSSLDTO{ - WebsiteSSL: model, - LogPath: path.Join(constant.SSLLogDir, fmt.Sprintf("%s-ssl-%d.log", model.PrimaryDomain, model.ID)), - }) - } - return total, result, err -} - -func (w WebsiteSSLService) GetSSL(id uint) (*response.WebsiteSSLDTO, error) { - var res response.WebsiteSSLDTO - websiteSSL, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(id)) - if err != nil { - return nil, err - } - res.WebsiteSSL = *websiteSSL - return &res, nil -} - -func (w WebsiteSSLService) Search(search request.WebsiteSSLSearch) ([]response.WebsiteSSLDTO, error) { - var ( - opts []repo.DBOption - result []response.WebsiteSSLDTO - ) - opts = append(opts, commonRepo.WithOrderBy("created_at desc")) - if search.AcmeAccountID != "" { - acmeAccountID, err := strconv.ParseUint(search.AcmeAccountID, 10, 64) - if err != nil { - return nil, err - } - opts = append(opts, websiteSSLRepo.WithByAcmeAccountId(uint(acmeAccountID))) - } - sslList, err := websiteSSLRepo.List(opts...) - if err != nil { - return nil, err - } - for _, sslModel := range sslList { - result = append(result, response.WebsiteSSLDTO{ - WebsiteSSL: sslModel, - }) - } - return result, err -} - -func (w WebsiteSSLService) Create(create request.WebsiteSSLCreate) (request.WebsiteSSLCreate, error) { - if create.Nameserver1 != "" && !common.IsValidIP(create.Nameserver1) { - return create, buserr.New("ErrParseIP") - } - if create.Nameserver2 != "" && !common.IsValidIP(create.Nameserver2) { - return create, buserr.New("ErrParseIP") - } - var res request.WebsiteSSLCreate - acmeAccount, err := websiteAcmeRepo.GetFirst(commonRepo.WithByID(create.AcmeAccountID)) - if err != nil { - return res, err - } - websiteSSL := model.WebsiteSSL{ - Status: constant.SSLInit, - Provider: create.Provider, - AcmeAccountID: acmeAccount.ID, - PrimaryDomain: create.PrimaryDomain, - ExpireDate: time.Now(), - KeyType: create.KeyType, - PushDir: create.PushDir, - Description: create.Description, - Nameserver1: create.Nameserver1, - Nameserver2: create.Nameserver2, - SkipDNS: create.SkipDNS, - DisableCNAME: create.DisableCNAME, - ExecShell: create.ExecShell, - } - if create.ExecShell { - websiteSSL.Shell = create.Shell - } - if create.PushDir { - fileOP := files.NewFileOp() - if !fileOP.Stat(create.Dir) { - _ = fileOP.CreateDir(create.Dir, 0755) - } - websiteSSL.Dir = create.Dir - } - - var domains []string - if create.OtherDomains != "" { - otherDomainArray := strings.Split(create.OtherDomains, "\n") - for _, domain := range otherDomainArray { - if !common.IsValidDomain(domain) { - err = buserr.WithName("ErrDomainFormat", domain) - return res, err - } - domains = append(domains, domain) - } - } - websiteSSL.Domains = strings.Join(domains, ",") - - if create.Provider == constant.DNSAccount || create.Provider == constant.Http { - websiteSSL.AutoRenew = create.AutoRenew - } - if create.Provider == constant.DNSAccount { - dnsAccount, err := websiteDnsRepo.GetFirst(commonRepo.WithByID(create.DnsAccountID)) - if err != nil { - return res, err - } - websiteSSL.DnsAccountID = dnsAccount.ID - } - - if err := websiteSSLRepo.Create(context.TODO(), &websiteSSL); err != nil { - return res, err - } - create.ID = websiteSSL.ID - go func() { - if create.Provider != constant.DnsManual { - if err = w.ObtainSSL(request.WebsiteSSLApply{ - ID: websiteSSL.ID, - }); err != nil { - global.LOG.Errorf("obtain ssl failed, err: %v", err) - } - } - }() - return create, nil -} - -func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error { - var ( - err error - websiteSSL *model.WebsiteSSL - acmeAccount *model.WebsiteAcmeAccount - dnsAccount *model.WebsiteDnsAccount - ) - - websiteSSL, err = websiteSSLRepo.GetFirst(commonRepo.WithByID(apply.ID)) - if err != nil { - return err - } - acmeAccount, err = websiteAcmeRepo.GetFirst(commonRepo.WithByID(websiteSSL.AcmeAccountID)) - if err != nil { - return err - } - client, err := ssl.NewAcmeClient(acmeAccount) - if err != nil { - return err - } - - switch websiteSSL.Provider { - case constant.DNSAccount: - dnsAccount, err = websiteDnsRepo.GetFirst(commonRepo.WithByID(websiteSSL.DnsAccountID)) - if err != nil { - return err - } - if err = client.UseDns(ssl.DnsType(dnsAccount.Type), dnsAccount.Authorization, *websiteSSL); err != nil { - return err - } - case constant.Http: - appInstall, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - if gorm.IsRecordNotFoundError(err) { - return buserr.New("ErrOpenrestyNotFound") - } - return err - } - if err := client.UseHTTP(path.Join(appInstall.GetPath(), "root")); err != nil { - return err - } - case constant.DnsManual: - if err := client.UseManualDns(); err != nil { - return err - } - } - - domains := []string{websiteSSL.PrimaryDomain} - if websiteSSL.Domains != "" { - domains = append(domains, strings.Split(websiteSSL.Domains, ",")...) - } - - var privateKey crypto.PrivateKey - if websiteSSL.PrivateKey == "" { - privateKey, err = certcrypto.GeneratePrivateKey(ssl.KeyType(websiteSSL.KeyType)) - if err != nil { - return err - } - } else { - block, _ := pem.Decode([]byte(websiteSSL.PrivateKey)) - if block == nil { - return buserr.New("invalid PEM block") - } - var privKey crypto.PrivateKey - keyType := ssl.KeyType(websiteSSL.KeyType) - switch keyType { - case certcrypto.EC256, certcrypto.EC384: - privKey, err = x509.ParseECPrivateKey(block.Bytes) - if err != nil { - return nil - } - case certcrypto.RSA2048, certcrypto.RSA3072, certcrypto.RSA4096: - privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - return nil - } - } - privateKey = privKey - } - - websiteSSL.Status = constant.SSLApply - err = websiteSSLRepo.Save(websiteSSL) - if err != nil { - return err - } - - go func() { - logFile, _ := os.OpenFile(path.Join(constant.SSLLogDir, fmt.Sprintf("%s-ssl-%d.log", websiteSSL.PrimaryDomain, websiteSSL.ID)), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666) - defer logFile.Close() - logger := log.New(logFile, "", log.LstdFlags) - legoLogger.Logger = logger - startMsg := i18n.GetMsgWithMap("ApplySSLStart", map[string]interface{}{"domain": strings.Join(domains, ","), "type": i18n.GetMsgByKey(websiteSSL.Provider)}) - if websiteSSL.Provider == constant.DNSAccount { - startMsg = startMsg + i18n.GetMsgWithMap("DNSAccountName", map[string]interface{}{"name": dnsAccount.Name, "type": dnsAccount.Type}) - } - legoLogger.Logger.Println(startMsg) - resource, err := client.ObtainSSL(domains, privateKey) - if err != nil { - handleError(websiteSSL, err) - return - } - websiteSSL.PrivateKey = string(resource.PrivateKey) - websiteSSL.Pem = string(resource.Certificate) - websiteSSL.CertURL = resource.CertURL - certBlock, _ := pem.Decode(resource.Certificate) - cert, err := x509.ParseCertificate(certBlock.Bytes) - if err != nil { - handleError(websiteSSL, err) - return - } - websiteSSL.ExpireDate = cert.NotAfter - websiteSSL.StartDate = cert.NotBefore - websiteSSL.Type = cert.Issuer.CommonName - websiteSSL.Organization = cert.Issuer.Organization[0] - websiteSSL.Status = constant.SSLReady - legoLogger.Logger.Println(i18n.GetMsgWithMap("ApplySSLSuccess", map[string]interface{}{"domain": strings.Join(domains, ",")})) - saveCertificateFile(websiteSSL, logger) - - if websiteSSL.ExecShell { - workDir := constant.DataDir - if websiteSSL.PushDir { - workDir = websiteSSL.Dir - } - legoLogger.Logger.Println(i18n.GetMsgByKey("ExecShellStart")) - if err = cmd.ExecShellWithTimeOut(websiteSSL.Shell, workDir, logger, 30*time.Minute); err != nil { - legoLogger.Logger.Println(i18n.GetMsgWithMap("ErrExecShell", map[string]interface{}{"err": err.Error()})) - } else { - legoLogger.Logger.Println(i18n.GetMsgByKey("ExecShellSuccess")) - } - } - - err = websiteSSLRepo.Save(websiteSSL) - if err != nil { - return - } - - websites, _ := websiteRepo.GetBy(websiteRepo.WithWebsiteSSLID(websiteSSL.ID)) - if len(websites) > 0 { - for _, website := range websites { - legoLogger.Logger.Println(i18n.GetMsgWithMap("ApplyWebSiteSSLLog", map[string]interface{}{"name": website.PrimaryDomain})) - if err := createPemFile(website, *websiteSSL); err != nil { - legoLogger.Logger.Println(i18n.GetMsgWithMap("ErrUpdateWebsiteSSL", map[string]interface{}{"name": website.PrimaryDomain, "err": err.Error()})) - } - } - nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return - } - if err := opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil { - legoLogger.Logger.Println(i18n.GetMsgByKey(constant.ErrSSLApply)) - return - } - legoLogger.Logger.Println(i18n.GetMsgByKey("ApplyWebSiteSSLSuccess")) - } - }() - - return nil -} - -func handleError(websiteSSL *model.WebsiteSSL, err error) { - if websiteSSL.Status == constant.SSLInit || websiteSSL.Status == constant.SSLError { - websiteSSL.Status = constant.Error - } else { - websiteSSL.Status = constant.SSLApplyError - } - websiteSSL.Message = err.Error() - legoLogger.Logger.Println(i18n.GetErrMsg("ApplySSLFailed", map[string]interface{}{"domain": websiteSSL.PrimaryDomain, "detail": err.Error()})) - _ = websiteSSLRepo.Save(websiteSSL) -} - -func (w WebsiteSSLService) GetDNSResolve(req request.WebsiteDNSReq) ([]response.WebsiteDNSRes, error) { - acmeAccount, err := websiteAcmeRepo.GetFirst(commonRepo.WithByID(req.AcmeAccountID)) - if err != nil { - return nil, err - } - - client, err := ssl.NewAcmeClient(acmeAccount) - if err != nil { - return nil, err - } - resolves, err := client.GetDNSResolve(req.Domains) - if err != nil { - return nil, err - } - var res []response.WebsiteDNSRes - for k, v := range resolves { - res = append(res, response.WebsiteDNSRes{ - Domain: k, - Key: v.Key, - Value: v.Value, - Err: v.Err, - }) - } - return res, nil -} - -func (w WebsiteSSLService) GetWebsiteSSL(websiteId uint) (response.WebsiteSSLDTO, error) { - var res response.WebsiteSSLDTO - website, err := websiteRepo.GetFirst(commonRepo.WithByID(websiteId)) - if err != nil { - return res, err - } - websiteSSL, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(website.WebsiteSSLID)) - if err != nil { - return res, err - } - res.WebsiteSSL = *websiteSSL - return res, nil -} - -func (w WebsiteSSLService) Delete(ids []uint) error { - var names []string - for _, id := range ids { - if websites, _ := websiteRepo.GetBy(websiteRepo.WithWebsiteSSLID(id)); len(websites) > 0 { - oldSSL, _ := websiteSSLRepo.GetFirst(commonRepo.WithByID(id)) - if oldSSL.ID > 0 { - names = append(names, oldSSL.PrimaryDomain) - } - continue - } - sslSetting, _ := settingRepo.Get(settingRepo.WithByKey("SSL")) - if sslSetting.Value == "enable" { - sslID, _ := settingRepo.Get(settingRepo.WithByKey("SSLID")) - idValue, _ := strconv.Atoi(sslID.Value) - if idValue > 0 && uint(idValue) == id { - return buserr.New("ErrDeleteWithPanelSSL") - } - } - _ = websiteSSLRepo.DeleteBy(commonRepo.WithByID(id)) - } - if len(names) > 0 { - return buserr.WithName("ErrSSLCannotDelete", strings.Join(names, ",")) - } - return nil -} - -func (w WebsiteSSLService) Update(update request.WebsiteSSLUpdate) error { - websiteSSL, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(update.ID)) - if err != nil { - return err - } - updateParams := make(map[string]interface{}) - updateParams["primary_domain"] = update.PrimaryDomain - updateParams["description"] = update.Description - updateParams["provider"] = update.Provider - updateParams["push_dir"] = update.PushDir - updateParams["disable_cname"] = update.DisableCNAME - updateParams["skip_dns"] = update.SkipDNS - updateParams["nameserver1"] = update.Nameserver1 - updateParams["nameserver2"] = update.Nameserver2 - updateParams["exec_shell"] = update.ExecShell - if update.ExecShell { - updateParams["shell"] = update.Shell - } else { - updateParams["shell"] = "" - } - - if websiteSSL.Provider != constant.SelfSigned { - acmeAccount, err := websiteAcmeRepo.GetFirst(commonRepo.WithByID(update.AcmeAccountID)) - if err != nil { - return err - } - updateParams["acme_account_id"] = acmeAccount.ID - } - - if update.PushDir { - fileOP := files.NewFileOp() - if !fileOP.Stat(update.Dir) { - _ = fileOP.CreateDir(update.Dir, 0755) - } - updateParams["dir"] = update.Dir - } - var domains []string - if update.OtherDomains != "" { - otherDomainArray := strings.Split(update.OtherDomains, "\n") - for _, domain := range otherDomainArray { - if !common.IsValidDomain(domain) { - return buserr.WithName("ErrDomainFormat", domain) - } - domains = append(domains, domain) - } - } - updateParams["domains"] = strings.Join(domains, ",") - if update.Provider == constant.DNSAccount || update.Provider == constant.Http || update.Provider == constant.SelfSigned { - updateParams["auto_renew"] = update.AutoRenew - } else { - updateParams["auto_renew"] = false - } - if update.Provider == constant.DNSAccount { - dnsAccount, err := websiteDnsRepo.GetFirst(commonRepo.WithByID(update.DnsAccountID)) - if err != nil { - return err - } - updateParams["dns_account_id"] = dnsAccount.ID - } - return websiteSSLRepo.SaveByMap(websiteSSL, updateParams) -} - -func (w WebsiteSSLService) Upload(req request.WebsiteSSLUpload) error { - websiteSSL := &model.WebsiteSSL{ - Provider: constant.Manual, - Description: req.Description, - Status: constant.SSLReady, - } - var err error - if req.SSLID > 0 { - websiteSSL, err = websiteSSLRepo.GetFirst(commonRepo.WithByID(req.SSLID)) - if err != nil { - return err - } - websiteSSL.Description = req.Description - } - if req.Type == "local" { - fileOp := files.NewFileOp() - if !fileOp.Stat(req.PrivateKeyPath) { - return buserr.New("ErrSSLKeyNotFound") - } - if !fileOp.Stat(req.CertificatePath) { - return buserr.New("ErrSSLCertificateNotFound") - } - if content, err := fileOp.GetContent(req.PrivateKeyPath); err != nil { - return err - } else { - websiteSSL.PrivateKey = string(content) - } - if content, err := fileOp.GetContent(req.CertificatePath); err != nil { - return err - } else { - websiteSSL.Pem = string(content) - } - } else { - websiteSSL.PrivateKey = req.PrivateKey - websiteSSL.Pem = req.Certificate - } - - privateKeyCertBlock, _ := pem.Decode([]byte(websiteSSL.PrivateKey)) - if privateKeyCertBlock == nil { - return buserr.New("ErrSSLKeyFormat") - } - - var ( - cert *x509.Certificate - pemData = []byte(websiteSSL.Pem) - ) - for { - certBlock, reset := pem.Decode(pemData) - if certBlock == nil { - break - } - cert, err = x509.ParseCertificate(certBlock.Bytes) - if err != nil { - return err - } - if len(cert.DNSNames) > 0 || len(cert.IPAddresses) > 0 { - break - } - pemData = reset - } - if pemData == nil { - return buserr.New("ErrSSLCertificateFormat") - } - - websiteSSL.ExpireDate = cert.NotAfter - websiteSSL.StartDate = cert.NotBefore - websiteSSL.Type = cert.Issuer.CommonName - if len(cert.Issuer.Organization) > 0 { - websiteSSL.Organization = cert.Issuer.Organization[0] - } else { - websiteSSL.Organization = cert.Issuer.CommonName - } - - var domains []string - if len(cert.DNSNames) > 0 { - websiteSSL.PrimaryDomain = cert.DNSNames[0] - domains = cert.DNSNames[1:] - } - if len(cert.IPAddresses) > 0 { - if websiteSSL.PrimaryDomain == "" { - websiteSSL.PrimaryDomain = cert.IPAddresses[0].String() - for _, ip := range cert.IPAddresses[1:] { - domains = append(domains, ip.String()) - } - } else { - for _, ip := range cert.IPAddresses { - domains = append(domains, ip.String()) - } - } - } - websiteSSL.Domains = strings.Join(domains, ",") - - if websiteSSL.ID > 0 { - if err := UpdateSSLConfig(*websiteSSL); err != nil { - return err - } - return websiteSSLRepo.Save(websiteSSL) - } - return websiteSSLRepo.Create(context.Background(), websiteSSL) -} - -func (w WebsiteSSLService) DownloadFile(id uint) (*os.File, error) { - websiteSSL, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(id)) - if err != nil { - return nil, err - } - fileOp := files.NewFileOp() - dir := path.Join(global.CONF.System.BaseDir, "1panel/tmp/ssl", websiteSSL.PrimaryDomain) - if fileOp.Stat(dir) { - if err = fileOp.DeleteDir(dir); err != nil { - return nil, err - } - } - if err = fileOp.CreateDir(dir, 0666); err != nil { - return nil, err - } - if err = fileOp.WriteFile(path.Join(dir, "fullchain.pem"), strings.NewReader(websiteSSL.Pem), 0644); err != nil { - return nil, err - } - if err = fileOp.WriteFile(path.Join(dir, "privkey.pem"), strings.NewReader(websiteSSL.PrivateKey), 0644); err != nil { - return nil, err - } - fileName := websiteSSL.PrimaryDomain + ".zip" - if err = fileOp.Compress([]string{path.Join(dir, "fullchain.pem"), path.Join(dir, "privkey.pem")}, dir, fileName, files.SdkZip, ""); err != nil { - return nil, err - } - return os.Open(path.Join(dir, fileName)) -} - -func (w WebsiteSSLService) SyncForRestart() error { - sslList, err := websiteSSLRepo.List() - if err != nil { - return err - } - for _, ssl := range sslList { - if ssl.Status == constant.SSLApply { - ssl.Status = constant.SystemRestart - ssl.Message = "System restart causing interrupt" - _ = websiteSSLRepo.Save(&ssl) - } - } - return nil -} diff --git a/backend/app/service/website_utils.go b/backend/app/service/website_utils.go deleted file mode 100644 index bad21c998..000000000 --- a/backend/app/service/website_utils.go +++ /dev/null @@ -1,1117 +0,0 @@ -package service - -import ( - "encoding/json" - "fmt" - "github.com/1Panel-dev/1Panel/backend/utils/xpack" - "log" - "os" - "path" - "path/filepath" - "reflect" - "strconv" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/i18n" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/1Panel-dev/1Panel/backend/utils/nginx/components" - "gopkg.in/yaml.v3" - - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/1Panel-dev/1Panel/backend/utils/nginx" - "github.com/1Panel-dev/1Panel/backend/utils/nginx/parser" - "github.com/1Panel-dev/1Panel/cmd/server/nginx_conf" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -func getDomain(domainStr string, defaultPort int) (model.WebsiteDomain, error) { - var ( - err error - domain = model.WebsiteDomain{} - portN int - ) - domainArray := strings.Split(domainStr, ":") - if len(domainArray) == 1 { - domain.Domain, err = handleChineseDomain(domainArray[0]) - if err != nil { - return domain, err - } - domain.Port = defaultPort - return domain, nil - } - if len(domainArray) > 1 { - domain.Domain, err = handleChineseDomain(domainArray[0]) - if err != nil { - return domain, err - } - portStr := domainArray[1] - portN, err = strconv.Atoi(portStr) - if err != nil { - return domain, buserr.WithName("ErrTypePort", portStr) - } - if portN <= 0 || portN > 65535 { - return domain, buserr.New("ErrTypePortRange") - } - domain.Port = portN - return domain, nil - } - return domain, nil -} - -func handleChineseDomain(domain string) (string, error) { - if common.ContainsChinese(domain) { - return common.PunycodeEncode(domain) - } - return domain, nil -} - -func createIndexFile(website *model.Website, runtime *model.Runtime) error { - nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return err - } - var ( - indexPath string - indexContent string - websiteService = NewIWebsiteService() - indexFolder = path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "index") - ) - - switch website.Type { - case constant.Static: - indexPath = path.Join(indexFolder, "index.html") - indexHtml, _ := websiteService.GetDefaultHtml("index") - indexContent = indexHtml.Content - case constant.Runtime: - if runtime.Type == constant.RuntimePHP { - indexPath = path.Join(indexFolder, "index.php") - indexPhp, _ := websiteService.GetDefaultHtml("php") - indexContent = indexPhp.Content - } - } - - fileOp := files.NewFileOp() - if !fileOp.Stat(indexFolder) { - if err := fileOp.CreateDir(indexFolder, 0755); err != nil { - return err - } - } - if !fileOp.Stat(indexPath) { - if err := fileOp.CreateFile(indexPath); err != nil { - return err - } - } - if website.Type == constant.Runtime && runtime.Resource == constant.ResourceAppstore { - if err := chownRootDir(indexFolder); err != nil { - return err - } - } - if err := fileOp.WriteFile(indexPath, strings.NewReader(indexContent), 0755); err != nil { - return err - } - - html404, _ := websiteService.GetDefaultHtml("404") - path404 := path.Join(indexFolder, "404.html") - if err := fileOp.WriteFile(path404, strings.NewReader(html404.Content), 0755); err != nil { - return err - } - - return nil -} - -func createProxyFile(website *model.Website) error { - nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return err - } - proxyFolder := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "www", "sites", website.Alias, "proxy") - filePath := path.Join(proxyFolder, "root.conf") - fileOp := files.NewFileOp() - if !fileOp.Stat(proxyFolder) { - if err := fileOp.CreateDir(proxyFolder, 0755); err != nil { - return err - } - } - if !fileOp.Stat(filePath) { - if err := fileOp.CreateFile(filePath); err != nil { - return err - } - } - config, err := parser.NewStringParser(string(nginx_conf.Proxy)).Parse() - if err != nil { - return err - } - config.FilePath = filePath - directives := config.Directives - location, ok := directives[0].(*components.Location) - if !ok { - return errors.New("error") - } - location.ChangePath("^~", "/") - location.UpdateDirective("proxy_pass", []string{website.Proxy}) - location.UpdateDirective("proxy_set_header", []string{"Host", "$host"}) - if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { - return buserr.WithErr(constant.ErrUpdateBuWebsite, err) - } - return nil -} - -func createWebsiteFolder(nginxInstall model.AppInstall, website *model.Website, runtime *model.Runtime) error { - nginxFolder := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name) - siteFolder := path.Join(nginxFolder, "www", "sites", website.Alias) - fileOp := files.NewFileOp() - if !fileOp.Stat(siteFolder) { - if err := fileOp.CreateDir(siteFolder, 0755); err != nil { - return err - } - if err := fileOp.CreateDir(path.Join(siteFolder, "log"), 0755); err != nil { - return err - } - if err := fileOp.CreateFile(path.Join(siteFolder, "log", "access.log")); err != nil { - return err - } - if err := fileOp.CreateFile(path.Join(siteFolder, "log", "error.log")); err != nil { - return err - } - if err := fileOp.CreateDir(path.Join(siteFolder, "index"), 0775); err != nil { - return err - } - if err := fileOp.CreateDir(path.Join(siteFolder, "ssl"), 0755); err != nil { - return err - } - if website.Type == constant.Runtime { - if runtime.Type == constant.RuntimePHP && runtime.Resource == constant.ResourceLocal { - phpPoolDir := path.Join(siteFolder, "php-pool") - if err := fileOp.CreateDir(phpPoolDir, 0755); err != nil { - return err - } - if err := fileOp.CreateFile(path.Join(phpPoolDir, "php-fpm.sock")); err != nil { - return err - } - } - } - if website.Type == constant.Static || (website.Type == constant.Runtime && runtime.Type == constant.RuntimePHP) { - if err := createIndexFile(website, runtime); err != nil { - return err - } - } - if website.Type == constant.Proxy { - if err := createProxyFile(website); err != nil { - return err - } - } - } - return nil -} - -func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, appInstall *model.AppInstall, runtime *model.Runtime) error { - nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return err - } - if err := createWebsiteFolder(nginxInstall, website, runtime); err != nil { - return err - } - - nginxFileName := website.Alias + ".conf" - configPath := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "conf", "conf.d", nginxFileName) - nginxContent := string(nginx_conf.WebsiteDefault) - config, err := parser.NewStringParser(nginxContent).Parse() - if err != nil { - return err - } - servers := config.FindServers() - if len(servers) == 0 { - return errors.New("nginx config is not valid") - } - server := servers[0] - server.DeleteListen("80") - var serverNames []string - for _, domain := range domains { - serverNames = append(serverNames, domain.Domain) - server.UpdateListen(strconv.Itoa(domain.Port), false) - if website.IPV6 { - server.UpdateListen("[::]:"+strconv.Itoa(domain.Port), false) - } - } - server.UpdateServerName(serverNames) - - siteFolder := path.Join("/www", "sites", website.Alias) - server.UpdateDirective("access_log", []string{path.Join(siteFolder, "log", "access.log"), "main"}) - server.UpdateDirective("error_log", []string{path.Join(siteFolder, "log", "error.log")}) - - rootIndex := path.Join("/www/sites", website.Alias, "index") - switch website.Type { - case constant.Deployment: - proxy := fmt.Sprintf("http://127.0.0.1:%d", appInstall.HttpPort) - server.UpdateRootProxy([]string{proxy}) - case constant.Static: - server.UpdateRoot(rootIndex) - server.UpdateDirective("error_page", []string{"404", "/404.html"}) - case constant.Proxy: - nginxInclude := fmt.Sprintf("/www/sites/%s/proxy/*.conf", website.Alias) - server.UpdateDirective("include", []string{nginxInclude}) - case constant.Runtime: - switch runtime.Type { - case constant.RuntimePHP: - server.UpdateDirective("error_page", []string{"404", "/404.html"}) - if runtime.Resource == constant.ResourceLocal { - switch runtime.Type { - case constant.RuntimePHP: - server.UpdateRoot(rootIndex) - localPath := path.Join(nginxInstall.GetPath(), rootIndex, "index.php") - server.UpdatePHPProxy([]string{website.Proxy}, localPath) - } - } else { - server.UpdateRoot(rootIndex) - server.UpdatePHPProxy([]string{website.Proxy}, "") - } - case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo: - proxy := fmt.Sprintf("http://127.0.0.1:%d", runtime.Port) - server.UpdateRootProxy([]string{proxy}) - } - } - - config.FilePath = configPath - if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { - return err - } - if err := opNginx(nginxInstall.ContainerName, constant.NginxCheck); err != nil { - _ = deleteWebsiteFolder(nginxInstall, website) - return err - } - if err := opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil { - _ = deleteWebsiteFolder(nginxInstall, website) - return err - } - return nil -} - -func createWafConfig(website *model.Website, domains []model.WebsiteDomain) error { - nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return err - } - - if !common.CompareVersion(nginxInstall.Version, "1.21.4.3-2-0") { - return nil - } - wafDataPath := path.Join(nginxInstall.GetPath(), "1pwaf", "data") - fileOp := files.NewFileOp() - if !fileOp.Stat(wafDataPath) { - return nil - } - websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json") - content, err := fileOp.GetContent(websitesConfigPath) - if err != nil { - return err - } - var websitesArray []request.WafWebsite - if len(content) != 0 { - if err := json.Unmarshal(content, &websitesArray); err != nil { - return err - } - } - wafWebsite := request.WafWebsite{ - Key: website.Alias, - Domains: make([]string, 0), - Host: make([]string, 0), - } - - for _, domain := range domains { - wafWebsite.Domains = append(wafWebsite.Domains, domain.Domain) - wafWebsite.Host = append(wafWebsite.Host, domain.Domain+":"+strconv.Itoa(domain.Port)) - } - websitesArray = append(websitesArray, wafWebsite) - websitesContent, err := json.Marshal(websitesArray) - if err != nil { - return err - } - if err := fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, 0644); err != nil { - return err - } - - var ( - sitesDir = path.Join(wafDataPath, "sites") - defaultConfigPath = path.Join(wafDataPath, "conf", "siteConfig.json") - defaultRuleDir = path.Join(wafDataPath, "rules") - websiteDir = path.Join(sitesDir, website.Alias) - ) - - defaultConfigContent, err := fileOp.GetContent(defaultConfigPath) - if err != nil { - return err - } - - if !fileOp.Stat(websiteDir) { - if err = fileOp.CreateDir(websiteDir, 0755); err != nil { - return err - } - } - defer func() { - if err != nil { - _ = fileOp.DeleteDir(websiteDir) - } - }() - - if err = fileOp.SaveFileWithByte(path.Join(websiteDir, "config.json"), defaultConfigContent, 0644); err != nil { - return err - } - - websiteRuleDir := path.Join(websiteDir, "rules") - if !fileOp.Stat(websiteRuleDir) { - if err := fileOp.CreateDir(websiteRuleDir, 0755); err != nil { - return err - } - } - defaultRulesName := []string{"acl", "args", "cookie", "defaultUaBlack", "defaultUrlBlack", "fileExt", "header", "methodWhite", "cdn"} - for _, ruleName := range defaultRulesName { - srcPath := path.Join(defaultRuleDir, ruleName+".json") - if fileOp.Stat(srcPath) { - _ = fileOp.Copy(srcPath, websiteRuleDir) - } - } - - if err = opNginx(nginxInstall.ContainerName, constant.NginxCheck); err != nil { - return err - } - if err = opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil { - return err - } - - return nil -} - -func delNginxConfig(website model.Website, force bool) error { - nginxApp, err := appRepo.GetFirst(appRepo.WithKey(constant.AppOpenresty)) - if err != nil { - return err - } - nginxInstall, err := appInstallRepo.GetFirst(appInstallRepo.WithAppId(nginxApp.ID)) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil - } - return err - } - - nginxFileName := website.Alias + ".conf" - configPath := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "conf", "conf.d", nginxFileName) - fileOp := files.NewFileOp() - - if !fileOp.Stat(configPath) { - return nil - } - if err := fileOp.DeleteFile(configPath); err != nil { - return err - } - sitePath := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "www", "sites", website.Alias) - if fileOp.Stat(sitePath) { - xpack.RemoveTamper(website.Alias) - _ = fileOp.DeleteDir(sitePath) - } - - if err := opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil { - if force { - return nil - } - return err - } - return nil -} - -func delWafConfig(website model.Website, force bool) error { - nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return err - } - if !common.CompareVersion(nginxInstall.Version, "1.21.4.3-2-0") { - return nil - } - wafDataPath := path.Join(nginxInstall.GetPath(), "1pwaf", "data") - fileOp := files.NewFileOp() - if !fileOp.Stat(wafDataPath) { - return nil - } - monitorDir := path.Join(wafDataPath, "db", "sites", website.Alias) - if fileOp.Stat(monitorDir) { - _ = fileOp.DeleteDir(monitorDir) - } - websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json") - content, err := fileOp.GetContent(websitesConfigPath) - if err != nil { - return err - } - var websitesArray []request.WafWebsite - var newWebsiteArray []request.WafWebsite - if len(content) > 0 { - if err = json.Unmarshal(content, &websitesArray); err != nil { - return err - } - } - for _, wafWebsite := range websitesArray { - if wafWebsite.Key != website.Alias { - newWebsiteArray = append(newWebsiteArray, wafWebsite) - } - } - websitesContent, err := json.Marshal(newWebsiteArray) - if err != nil { - return err - } - if err := fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, 0644); err != nil { - return err - } - - _ = fileOp.DeleteDir(path.Join(wafDataPath, "sites", website.Alias)) - - if err := opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil { - if force { - return nil - } - return err - } - return nil -} - -func addListenAndServerName(website model.Website, ports []int, domains []string) error { - nginxFull, err := getNginxFull(&website) - if err != nil { - return nil - } - nginxConfig := nginxFull.SiteConfig - config := nginxFull.SiteConfig.Config - server := config.FindServers()[0] - for _, port := range ports { - server.AddListen(strconv.Itoa(port), false) - if website.IPV6 { - server.UpdateListen("[::]:"+strconv.Itoa(port), false) - } - } - for _, domain := range domains { - server.AddServerName(domain) - } - if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { - return err - } - return nginxCheckAndReload(nginxConfig.OldContent, nginxConfig.FilePath, nginxFull.Install.ContainerName) -} - -func deleteListenAndServerName(website model.Website, binds []string, domains []string) error { - nginxFull, err := getNginxFull(&website) - if err != nil { - return nil - } - nginxConfig := nginxFull.SiteConfig - config := nginxFull.SiteConfig.Config - server := config.FindServers()[0] - for _, bind := range binds { - server.DeleteListen(bind) - server.DeleteListen("[::]:" + bind) - } - for _, domain := range domains { - server.DeleteServerName(domain) - } - - if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { - return err - } - return nginxCheckAndReload(nginxConfig.OldContent, nginxConfig.FilePath, nginxFull.Install.ContainerName) -} - -func createPemFile(website model.Website, websiteSSL model.WebsiteSSL) error { - nginxApp, err := appRepo.GetFirst(appRepo.WithKey(constant.AppOpenresty)) - if err != nil { - return err - } - nginxInstall, err := appInstallRepo.GetFirst(appInstallRepo.WithAppId(nginxApp.ID)) - if err != nil { - return err - } - - configDir := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "www", "sites", website.Alias, "ssl") - fileOp := files.NewFileOp() - - if !fileOp.Stat(configDir) { - if err := fileOp.CreateDir(configDir, 0775); err != nil { - return err - } - } - - fullChainFile := path.Join(configDir, "fullchain.pem") - privatePemFile := path.Join(configDir, "privkey.pem") - - if !fileOp.Stat(fullChainFile) { - if err := fileOp.CreateFile(fullChainFile); err != nil { - return err - } - } - if !fileOp.Stat(privatePemFile) { - if err := fileOp.CreateFile(privatePemFile); err != nil { - return err - } - } - - if err := fileOp.WriteFile(fullChainFile, strings.NewReader(websiteSSL.Pem), 0644); err != nil { - return err - } - if err := fileOp.WriteFile(privatePemFile, strings.NewReader(websiteSSL.PrivateKey), 0644); err != nil { - return err - } - return nil -} - -func applySSL(website model.Website, websiteSSL model.WebsiteSSL, req request.WebsiteHTTPSOp) error { - nginxFull, err := getNginxFull(&website) - if err != nil { - return nil - } - domains, err := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(website.ID)) - if err != nil { - return nil - } - noDefaultPort := true - for _, domain := range domains { - if domain.Port == 80 { - noDefaultPort = false - } - } - config := nginxFull.SiteConfig.Config - server := config.FindServers()[0] - - httpPort := strconv.Itoa(nginxFull.Install.HttpPort) - httpsPort := strconv.Itoa(nginxFull.Install.HttpsPort) - httpPortIPV6 := "[::]:" + httpPort - httpsPortIPV6 := "[::]:" + httpsPort - - server.UpdateListen(httpsPort, website.DefaultServer, "ssl", "http2") - if website.IPV6 { - server.UpdateListen(httpsPortIPV6, website.DefaultServer, "ssl", "http2") - } - - switch req.HttpConfig { - case constant.HTTPSOnly: - server.RemoveListenByBind(httpPort) - server.RemoveListenByBind(httpPortIPV6) - server.RemoveDirective("if", []string{"($scheme"}) - case constant.HTTPToHTTPS: - if !noDefaultPort { - server.UpdateListen(httpPort, website.DefaultServer) - } - if website.IPV6 { - server.UpdateListen(httpPortIPV6, website.DefaultServer) - } - server.AddHTTP2HTTPS() - case constant.HTTPAlso: - if !noDefaultPort { - server.UpdateListen(httpPort, website.DefaultServer) - } - server.RemoveDirective("if", []string{"($scheme"}) - if website.IPV6 { - server.UpdateListen(httpPortIPV6, website.DefaultServer) - } - } - - if !req.Hsts { - server.RemoveDirective("add_header", []string{"Strict-Transport-Security", "\"max-age=31536000\""}) - } - - if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { - return err - } - if err := createPemFile(website, websiteSSL); err != nil { - return err - } - nginxParams := getNginxParamsFromStaticFile(dto.SSL, []dto.NginxParam{}) - for i, param := range nginxParams { - if param.Name == "ssl_certificate" { - nginxParams[i].Params = []string{path.Join("/www", "sites", website.Alias, "ssl", "fullchain.pem")} - } - if param.Name == "ssl_certificate_key" { - nginxParams[i].Params = []string{path.Join("/www", "sites", website.Alias, "ssl", "privkey.pem")} - } - if param.Name == "ssl_protocols" { - nginxParams[i].Params = req.SSLProtocol - } - if param.Name == "ssl_ciphers" { - nginxParams[i].Params = []string{req.Algorithm} - } - } - if req.Hsts { - nginxParams = append(nginxParams, dto.NginxParam{ - Name: "add_header", - Params: []string{"Strict-Transport-Security", "\"max-age=31536000\""}, - }) - } - - if err := updateNginxConfig(constant.NginxScopeServer, nginxParams, &website); err != nil { - return err - } - return nil -} - -func getParamArray(key string, param interface{}) []string { - var res []string - switch p := param.(type) { - case string: - if key == "index" { - res = strings.Split(p, "\n") - return res - } - - res = strings.Split(p, " ") - return res - } - return res -} - -func handleParamMap(paramMap map[string]string, keys []string) []dto.NginxParam { - var nginxParams []dto.NginxParam - for k, v := range paramMap { - for _, name := range keys { - if name == k { - param := dto.NginxParam{ - Name: k, - Params: getParamArray(k, v), - } - nginxParams = append(nginxParams, param) - } - } - } - return nginxParams -} - -func getNginxParams(params interface{}, keys []string) []dto.NginxParam { - var nginxParams []dto.NginxParam - - switch p := params.(type) { - case map[string]interface{}: - return handleParamMap(toMapStr(p), keys) - case []interface{}: - for _, mA := range p { - if m, ok := mA.(map[string]interface{}); ok { - nginxParams = append(nginxParams, handleParamMap(toMapStr(m), keys)...) - } - } - } - return nginxParams -} - -func toMapStr(m map[string]interface{}) map[string]string { - ret := make(map[string]string, len(m)) - for k, v := range m { - ret[k] = fmt.Sprint(v) - } - return ret -} - -func deleteWebsiteFolder(nginxInstall model.AppInstall, website *model.Website) error { - nginxFolder := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name) - siteFolder := path.Join(nginxFolder, "www", "sites", website.Alias) - fileOp := files.NewFileOp() - if fileOp.Stat(siteFolder) { - _ = fileOp.DeleteDir(siteFolder) - } - nginxFilePath := path.Join(nginxFolder, "conf", "conf.d", website.PrimaryDomain+".conf") - if fileOp.Stat(nginxFilePath) { - _ = fileOp.DeleteFile(nginxFilePath) - } - return nil -} - -func opWebsite(website *model.Website, operate string) error { - nginxInstall, err := getNginxFull(website) - if err != nil { - return err - } - config := nginxInstall.SiteConfig.Config - servers := config.FindServers() - if len(servers) == 0 { - return errors.New("nginx config is not valid") - } - server := servers[0] - if operate == constant.StopWeb { - proxyInclude := fmt.Sprintf("/www/sites/%s/proxy/*.conf", website.Alias) - server.RemoveDirective("include", []string{proxyInclude}) - rewriteInclude := fmt.Sprintf("/www/sites/%s/rewrite/%s.conf", website.Alias, website.Alias) - server.RemoveDirective("include", []string{rewriteInclude}) - - switch website.Type { - case constant.Deployment: - server.RemoveDirective("location", []string{"/"}) - case constant.Runtime: - runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(website.RuntimeID)) - if err != nil { - return err - } - if runtime.Type == constant.RuntimePHP { - server.RemoveDirective("location", []string{"~", "[^/]\\.php(/|$)"}) - } else { - server.RemoveDirective("location", []string{"/"}) - } - } - server.UpdateRoot("/usr/share/nginx/html/stop") - website.Status = constant.WebStopped - } - if operate == constant.StartWeb { - absoluteIncludeDir := path.Join(nginxInstall.Install.GetPath(), fmt.Sprintf("/www/sites/%s/proxy", website.Alias)) - fileOp := files.NewFileOp() - if fileOp.Stat(absoluteIncludeDir) && !files.IsEmptyDir(absoluteIncludeDir) { - proxyInclude := fmt.Sprintf("/www/sites/%s/proxy/*.conf", website.Alias) - server.UpdateDirective("include", []string{proxyInclude}) - } - rewriteInclude := fmt.Sprintf("/www/sites/%s/rewrite/%s.conf", website.Alias, website.Alias) - absoluteRewritePath := path.Join(nginxInstall.Install.GetPath(), rewriteInclude) - if fileOp.Stat(absoluteRewritePath) { - server.UpdateDirective("include", []string{rewriteInclude}) - } - rootIndex := path.Join("/www/sites", website.Alias, "index") - if website.SiteDir != "/" { - rootIndex = path.Join(rootIndex, website.SiteDir) - } - switch website.Type { - case constant.Deployment: - server.RemoveDirective("root", nil) - appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID)) - if err != nil { - return err - } - proxy := fmt.Sprintf("http://127.0.0.1:%d", appInstall.HttpPort) - server.UpdateRootProxy([]string{proxy}) - case constant.Static: - server.UpdateRoot(rootIndex) - server.UpdateRootLocation() - case constant.Proxy: - server.RemoveDirective("root", nil) - case constant.Runtime: - server.UpdateRoot(rootIndex) - localPath := "" - runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(website.RuntimeID)) - if err != nil { - return err - } - if runtime.Type == constant.RuntimePHP { - if website.ProxyType == constant.RuntimeProxyUnix || website.ProxyType == constant.RuntimeProxyTcp { - localPath = path.Join(nginxInstall.Install.GetPath(), rootIndex, "index.php") - } - server.UpdatePHPProxy([]string{website.Proxy}, localPath) - } else { - proxy := fmt.Sprintf("http://127.0.0.1:%d", runtime.Port) - server.UpdateRootProxy([]string{proxy}) - } - } - website.Status = constant.WebRunning - now := time.Now() - if website.ExpireDate.Before(now) { - defaultDate, _ := time.Parse(constant.DateLayout, constant.DefaultDate) - website.ExpireDate = defaultDate - } - } - - if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { - return err - } - return nginxCheckAndReload(nginxInstall.SiteConfig.OldContent, config.FilePath, nginxInstall.Install.ContainerName) -} - -func changeIPV6(website model.Website, enable bool) error { - nginxFull, err := getNginxFull(&website) - if err != nil { - return nil - } - config := nginxFull.SiteConfig.Config - server := config.FindServers()[0] - listens := server.Listens - if enable { - for _, listen := range listens { - if strings.HasPrefix(listen.Bind, "[::]:") { - continue - } - exist := false - ipv6Bind := fmt.Sprintf("[::]:%s", listen.Bind) - for _, li := range listens { - if li.Bind == ipv6Bind { - exist = true - break - } - } - if !exist { - server.UpdateListen(ipv6Bind, false, listen.GetParameters()[1:]...) - } - } - } else { - for _, listen := range listens { - if strings.HasPrefix(listen.Bind, "[::]:") { - server.RemoveListenByBind(listen.Bind) - } - } - } - if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { - return err - } - return nginxCheckAndReload(nginxFull.SiteConfig.OldContent, config.FilePath, nginxFull.Install.ContainerName) -} - -func checkIsLinkApp(website model.Website) bool { - if website.Type == constant.Deployment { - return true - } - if website.Type == constant.Runtime { - runtime, _ := runtimeRepo.GetFirst(commonRepo.WithByID(website.RuntimeID)) - return runtime.Resource == constant.ResourceAppstore - } - return false -} - -func chownRootDir(path string) error { - _, err := cmd.ExecWithTimeOut(fmt.Sprintf(`chown -R 1000:1000 "%s"`, path), 1*time.Second) - if err != nil { - return err - } - return nil -} - -func changeServiceName(newComposeContent, newServiceName string) (composeByte []byte, err error) { - composeMap := make(map[string]interface{}) - if err = yaml.Unmarshal([]byte(newComposeContent), &composeMap); err != nil { - return - } - value, ok := composeMap["services"] - if !ok || value == nil { - err = buserr.New(constant.ErrFileParse) - return - } - servicesMap := value.(map[string]interface{}) - - index := 0 - serviceName := "" - for k := range servicesMap { - serviceName = k - if index > 0 { - continue - } - index++ - } - if newServiceName != serviceName { - servicesMap[newServiceName] = servicesMap[serviceName] - delete(servicesMap, serviceName) - } - - return yaml.Marshal(composeMap) -} - -func getWebsiteDomains(domains string, defaultPort int, websiteID uint) (domainModels []model.WebsiteDomain, addPorts []int, addDomains []string, err error) { - var ( - ports = make(map[int]struct{}) - ) - domainArray := strings.Split(domains, "\n") - for _, domain := range domainArray { - if domain == "" { - continue - } - if !common.IsValidDomain(domain) { - err = buserr.WithName("ErrDomainFormat", domain) - return - } - var domainModel model.WebsiteDomain - domainModel, err = getDomain(domain, defaultPort) - if err != nil { - return - } - if reflect.DeepEqual(domainModel, model.WebsiteDomain{}) { - continue - } - domainModel.WebsiteID = websiteID - domainModels = append(domainModels, domainModel) - if domainModel.Port != defaultPort { - ports[domainModel.Port] = struct{}{} - } - if exist, _ := websiteDomainRepo.GetFirst(websiteDomainRepo.WithDomain(domainModel.Domain), websiteDomainRepo.WithWebsiteId(websiteID)); exist.ID == 0 { - addDomains = append(addDomains, domainModel.Domain) - } - } - for _, domain := range domainModels { - if exist, _ := websiteDomainRepo.GetFirst(websiteDomainRepo.WithDomain(domain.Domain), websiteDomainRepo.WithPort(domain.Port)); exist.ID > 0 { - website, _ := websiteRepo.GetFirst(commonRepo.WithByID(exist.WebsiteID)) - err = buserr.WithName(constant.ErrDomainIsUsed, website.PrimaryDomain) - return - } - } - - for port := range ports { - if existPorts, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithPort(port)); len(existPorts) == 0 { - errMap := make(map[string]interface{}) - errMap["port"] = port - appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithPort(port)) - if appInstall.ID > 0 { - errMap["type"] = i18n.GetMsgByKey("TYPE_APP") - errMap["name"] = appInstall.Name - err = buserr.WithMap("ErrPortExist", errMap, nil) - return - } - runtime, _ := runtimeRepo.GetFirst(runtimeRepo.WithPort(port)) - if runtime != nil { - errMap["type"] = i18n.GetMsgByKey("TYPE_RUNTIME") - errMap["name"] = runtime.Name - err = buserr.WithMap("ErrPortExist", errMap, nil) - return - } - if common.ScanPort(port) { - err = buserr.WithDetail(constant.ErrPortInUsed, port, nil) - return - } - } - if existPorts, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(websiteID), websiteDomainRepo.WithPort(port)); len(existPorts) == 0 { - addPorts = append(addPorts, port) - } - } - - return -} - -func saveCertificateFile(websiteSSL *model.WebsiteSSL, logger *log.Logger) { - if websiteSSL.PushDir { - fileOp := files.NewFileOp() - var ( - pushErr error - MsgMap = map[string]interface{}{"path": websiteSSL.Dir, "status": i18n.GetMsgByKey("Success")} - ) - if pushErr = fileOp.SaveFile(path.Join(websiteSSL.Dir, "privkey.pem"), websiteSSL.PrivateKey, 0666); pushErr != nil { - MsgMap["status"] = i18n.GetMsgByKey("Failed") - logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap)) - logger.Println("Push dir failed:" + pushErr.Error()) - } - if pushErr = fileOp.SaveFile(path.Join(websiteSSL.Dir, "fullchain.pem"), websiteSSL.Pem, 0666); pushErr != nil { - MsgMap["status"] = i18n.GetMsgByKey("Failed") - logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap)) - logger.Println("Push dir failed:" + pushErr.Error()) - } - if pushErr == nil { - logger.Println(i18n.GetMsgWithMap("PushDirLog", MsgMap)) - } - } -} - -func GetSystemSSL() (bool, uint) { - sslSetting, err := settingRepo.Get(settingRepo.WithByKey("SSL")) - if err != nil { - global.LOG.Errorf("load service ssl from setting failed, err: %v", err) - return false, 0 - } - if sslSetting.Value == "enable" { - sslID, _ := settingRepo.Get(settingRepo.WithByKey("SSLID")) - idValue, _ := strconv.Atoi(sslID.Value) - if idValue > 0 { - return true, uint(idValue) - } - } - return false, 0 -} - -func UpdateSSLConfig(websiteSSL model.WebsiteSSL) error { - websites, _ := websiteRepo.GetBy(websiteRepo.WithWebsiteSSLID(websiteSSL.ID)) - if len(websites) > 0 { - for _, website := range websites { - if err := createPemFile(website, websiteSSL); err != nil { - return buserr.WithMap("ErrUpdateWebsiteSSL", map[string]interface{}{"name": website.PrimaryDomain, "err": err.Error()}, err) - } - } - nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) - if err != nil { - return err - } - if err := opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil { - return buserr.WithErr(constant.ErrSSLApply, err) - } - } - enable, sslID := GetSystemSSL() - if enable && sslID == websiteSSL.ID { - fileOp := files.NewFileOp() - secretDir := path.Join(global.CONF.System.BaseDir, "1panel/secret") - if err := fileOp.WriteFile(path.Join(secretDir, "server.crt"), strings.NewReader(websiteSSL.Pem), 0600); err != nil { - global.LOG.Errorf("Failed to update the SSL certificate File for 1Panel System domain [%s] , err:%s", websiteSSL.PrimaryDomain, err.Error()) - return err - } - if err := fileOp.WriteFile(path.Join(secretDir, "server.key"), strings.NewReader(websiteSSL.PrivateKey), 0600); err != nil { - global.LOG.Errorf("Failed to update the SSL certificate for 1Panel System domain [%s] , err:%s", websiteSSL.PrimaryDomain, err.Error()) - return err - } - } - return nil -} - -func ChangeHSTSConfig(enable bool, nginxInstall model.AppInstall, website model.Website) error { - includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "proxy") - fileOp := files.NewFileOp() - if !fileOp.Stat(includeDir) { - return nil - } - err := filepath.Walk(includeDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if !info.IsDir() { - if filepath.Ext(path) == ".conf" { - par, err := parser.NewParser(path) - if err != nil { - return err - } - config, err := par.Parse() - if err != nil { - return err - } - config.FilePath = path - directives := config.Directives - location, ok := directives[0].(*components.Location) - if !ok { - return nil - } - if enable { - location.UpdateDirective("add_header", []string{"Strict-Transport-Security", "\"max-age=31536000\""}) - } else { - location.RemoveDirective("add_header", []string{"Strict-Transport-Security", "\"max-age=31536000\""}) - } - if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { - return buserr.WithErr(constant.ErrUpdateBuWebsite, err) - } - } - } - return nil - }) - if err != nil { - return err - } - return nil -} - -func checkSSLStatus(expireDate time.Time) string { - now := time.Now() - daysUntilExpiry := int(expireDate.Sub(now).Hours() / 24) - - if daysUntilExpiry < 0 { - return "danger" - } else if daysUntilExpiry <= 10 { - return "warning" - } - return "success" -} - -func getResourceContent(fileOp files.FileOp, resourcePath string) (string, error) { - if fileOp.Stat(resourcePath) { - content, err := fileOp.GetContent(resourcePath) - if err != nil { - return "", err - } - return string(content), nil - } - return "", nil -} diff --git a/backend/app/task/task.go b/backend/app/task/task.go deleted file mode 100644 index 121d14904..000000000 --- a/backend/app/task/task.go +++ /dev/null @@ -1,115 +0,0 @@ -package task - -import ( - "context" - "fmt" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/i18n" - "log" - "os" - "path" - "strconv" - "time" -) - -type ActionFunc func() error -type RollbackFunc func() - -type Task struct { - Name string - Logger *log.Logger - SubTasks []*SubTask - Rollbacks []RollbackFunc - logFile *os.File -} - -type SubTask struct { - Name string - Retry int - Timeout time.Duration - Action ActionFunc - Rollback RollbackFunc - Error error -} - -func NewTask(name string, taskType string) (*Task, error) { - logPath := path.Join(constant.LogDir, taskType) - //TODO 增加插入到日志表的逻辑 - file, err := os.OpenFile(logPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0666) - if err != nil { - return nil, fmt.Errorf("failed to open log file: %w", err) - } - logger := log.New(file, "", log.LstdFlags) - return &Task{Name: name, logFile: file, Logger: logger}, nil -} - -func (t *Task) AddSubTask(name string, action ActionFunc, rollback RollbackFunc) { - subTask := &SubTask{Name: name, Retry: 0, Timeout: 10 * time.Minute, Action: action, Rollback: rollback} - t.SubTasks = append(t.SubTasks, subTask) -} - -func (t *Task) AddSubTaskWithOps(name string, action ActionFunc, rollback RollbackFunc, retry int, timeout time.Duration) { - subTask := &SubTask{Name: name, Retry: retry, Timeout: timeout, Action: action, Rollback: rollback} - t.SubTasks = append(t.SubTasks, subTask) -} - -func (s *SubTask) Execute(logger *log.Logger) bool { - logger.Printf(i18n.GetWithName("SubTaskStart", s.Name)) - for i := 0; i < s.Retry+1; i++ { - if i > 0 { - logger.Printf(i18n.GetWithName("TaskRetry", strconv.Itoa(i))) - } - ctx, cancel := context.WithTimeout(context.Background(), s.Timeout) - defer cancel() - - done := make(chan error) - go func() { - done <- s.Action() - }() - - select { - case <-ctx.Done(): - logger.Printf(i18n.GetWithName("TaskTimeout", s.Name)) - case err := <-done: - if err != nil { - s.Error = err - logger.Printf(i18n.GetWithNameAndErr("TaskFailed", s.Name, err)) - } else { - logger.Printf(i18n.GetWithName("TaskSuccess", s.Name)) - return true - } - } - - if i == s.Retry { - if s.Rollback != nil { - s.Rollback() - } - } - time.Sleep(1 * time.Second) - } - if s.Error != nil { - s.Error = fmt.Errorf(i18n.GetWithName("TaskFailed", s.Name)) - } - return false -} - -func (t *Task) Execute() error { - t.Logger.Printf(i18n.GetWithName("TaskStart", t.Name)) - var err error - for _, subTask := range t.SubTasks { - if subTask.Execute(t.Logger) { - if subTask.Rollback != nil { - t.Rollbacks = append(t.Rollbacks, subTask.Rollback) - } - } else { - err = subTask.Error - for _, rollback := range t.Rollbacks { - rollback() - } - break - } - } - t.Logger.Printf(i18n.GetWithName("TaskEnd", t.Name)) - _ = t.logFile.Close() - return err -} diff --git a/backend/buserr/errors.go b/backend/buserr/errors.go deleted file mode 100644 index ad3fe1152..000000000 --- a/backend/buserr/errors.go +++ /dev/null @@ -1,93 +0,0 @@ -package buserr - -import ( - "github.com/1Panel-dev/1Panel/backend/i18n" - "github.com/pkg/errors" -) - -type BusinessError struct { - Msg string - Detail interface{} - Map map[string]interface{} - Err error -} - -func (e BusinessError) Error() string { - content := "" - if e.Detail != nil { - content = i18n.GetErrMsg(e.Msg, map[string]interface{}{"detail": e.Detail}) - } else if e.Map != nil { - content = i18n.GetErrMsg(e.Msg, e.Map) - } else { - content = i18n.GetErrMsg(e.Msg, nil) - } - if content == "" { - if e.Err != nil { - return e.Err.Error() - } - return errors.New(e.Msg).Error() - } - return content -} - -func New(Key string) BusinessError { - return BusinessError{ - Msg: Key, - Detail: nil, - Err: nil, - } -} - -func WithDetail(Key string, detail interface{}, err error) BusinessError { - return BusinessError{ - Msg: Key, - Detail: detail, - Err: err, - } -} - -func WithErr(Key string, err error) BusinessError { - paramMap := map[string]interface{}{} - if err != nil { - paramMap["err"] = err - } - return BusinessError{ - Msg: Key, - Map: paramMap, - Err: err, - } -} - -func WithMap(Key string, maps map[string]interface{}, err error) BusinessError { - return BusinessError{ - Msg: Key, - Map: maps, - Err: err, - } -} - -func WithNameAndErr(Key string, name string, err error) BusinessError { - paramMap := map[string]interface{}{} - if name != "" { - paramMap["name"] = name - } - if err != nil { - paramMap["err"] = err.Error() - } - return BusinessError{ - Msg: Key, - Map: paramMap, - Err: err, - } -} - -func WithName(Key string, name string) BusinessError { - paramMap := map[string]interface{}{} - if name != "" { - paramMap["name"] = name - } - return BusinessError{ - Msg: Key, - Map: paramMap, - } -} diff --git a/backend/buserr/multi_err.go b/backend/buserr/multi_err.go deleted file mode 100644 index bf0c875ce..000000000 --- a/backend/buserr/multi_err.go +++ /dev/null @@ -1,23 +0,0 @@ -package buserr - -import ( - "bytes" - "fmt" - "sort" -) - -type MultiErr map[string]error - -func (e MultiErr) Error() string { - var keys []string - for key := range e { - keys = append(keys, key) - } - sort.Strings(keys) - - buffer := bytes.NewBufferString("") - for _, key := range keys { - buffer.WriteString(fmt.Sprintf("[%s] %s\n", key, e[key])) - } - return buffer.String() -} diff --git a/backend/configs/config.go b/backend/configs/config.go deleted file mode 100644 index 7d8116b68..000000000 --- a/backend/configs/config.go +++ /dev/null @@ -1,6 +0,0 @@ -package configs - -type ServerConfig struct { - System System `mapstructure:"system"` - LogConfig LogConfig `mapstructure:"log"` -} diff --git a/backend/configs/log.go b/backend/configs/log.go deleted file mode 100644 index 8fe60a91d..000000000 --- a/backend/configs/log.go +++ /dev/null @@ -1,9 +0,0 @@ -package configs - -type LogConfig struct { - Level string `mapstructure:"level"` - TimeZone string `mapstructure:"timeZone"` - LogName string `mapstructure:"log_name"` - LogSuffix string `mapstructure:"log_suffix"` - MaxBackup int `mapstructure:"max_backup"` -} diff --git a/backend/configs/system.go b/backend/configs/system.go deleted file mode 100644 index d6e7c87c9..000000000 --- a/backend/configs/system.go +++ /dev/null @@ -1,20 +0,0 @@ -package configs - -type System struct { - DbFile string `mapstructure:"db_file"` - DbPath string `mapstructure:"db_path"` - LogPath string `mapstructure:"log_path"` - DataDir string `mapstructure:"data_dir"` - TmpDir string `mapstructure:"tmp_dir"` - Cache string `mapstructure:"cache"` - Backup string `mapstructure:"backup"` - EncryptKey string `mapstructure:"encrypt_key"` - BaseDir string `mapstructure:"base_dir"` - Mode string `mapstructure:"mode"` - RepoUrl string `mapstructure:"repo_url"` - Version string `mapstructure:"version"` - IsDemo bool `mapstructure:"is_demo"` - AppRepo string `mapstructure:"app_repo"` - OneDriveID string `mapstructure:"one_drive_id"` - OneDriveSc string `mapstructure:"one_drive_sc"` -} diff --git a/backend/constant/app.go b/backend/constant/app.go deleted file mode 100644 index 9a6f5c825..000000000 --- a/backend/constant/app.go +++ /dev/null @@ -1,54 +0,0 @@ -package constant - -const ( - Running = "Running" - UnHealthy = "UnHealthy" - Error = "Error" - Stopped = "Stopped" - Installing = "Installing" - DownloadErr = "DownloadErr" - Upgrading = "Upgrading" - UpgradeErr = "UpgradeErr" - Rebuilding = "Rebuilding" - Syncing = "Syncing" - SyncSuccess = "SyncSuccess" - Paused = "Paused" - UpErr = "UpErr" - - ContainerPrefix = "1Panel-" - - AppNormal = "Normal" - AppTakeDown = "TakeDown" - - AppOpenresty = "openresty" - AppMysql = "mysql" - AppMariaDB = "mariadb" - AppPostgresql = "postgresql" - AppRedis = "redis" - AppPostgres = "postgres" - AppMongodb = "mongodb" - AppMemcached = "memcached" - - AppResourceLocal = "local" - AppResourceRemote = "remote" - - CPUS = "CPUS" - MemoryLimit = "MEMORY_LIMIT" - HostIP = "HOST_IP" - ContainerName = "CONTAINER_NAME" -) - -type AppOperate string - -var ( - Start AppOperate = "start" - Stop AppOperate = "stop" - Restart AppOperate = "restart" - Delete AppOperate = "delete" - Sync AppOperate = "sync" - Backup AppOperate = "backup" - Update AppOperate = "update" - Rebuild AppOperate = "rebuild" - Upgrade AppOperate = "upgrade" - Reload AppOperate = "reload" -) diff --git a/backend/constant/backup.go b/backend/constant/backup.go deleted file mode 100644 index 37baa994e..000000000 --- a/backend/constant/backup.go +++ /dev/null @@ -1,18 +0,0 @@ -package constant - -const ( - Valid = "VALID" - DisConnect = "DISCONNECT" - VerifyFailed = "VERIFYFAILED" - S3 = "S3" - OSS = "OSS" - Sftp = "SFTP" - OneDrive = "OneDrive" - MinIo = "MINIO" - Cos = "COS" - Kodo = "KODO" - WebDAV = "WebDAV" - Local = "LOCAL" - - OneDriveRedirectURI = "http://localhost/login/authorized" -) diff --git a/backend/constant/common.go b/backend/constant/common.go deleted file mode 100644 index cca64e21a..000000000 --- a/backend/constant/common.go +++ /dev/null @@ -1,25 +0,0 @@ -package constant - -type DBContext string - -const ( - DB DBContext = "db" - - SystemRestart = "systemRestart" - - TypeWebsite = "website" - TypePhp = "php" - TypeSSL = "ssl" - TypeSystem = "system" -) - -const ( - TimeOut5s = 5 - TimeOut20s = 20 - TimeOut5m = 300 - - DateLayout = "2006-01-02" // or use time.DateOnly while go version >= 1.20 - DefaultDate = "1970-01-01" - DateTimeLayout = "2006-01-02 15:04:05" // or use time.DateTime while go version >= 1.20 - DateTimeSlimLayout = "20060102150405" -) diff --git a/backend/constant/container.go b/backend/constant/container.go deleted file mode 100644 index 98853664c..000000000 --- a/backend/constant/container.go +++ /dev/null @@ -1,18 +0,0 @@ -package constant - -const ( - ContainerOpStart = "start" - ContainerOpStop = "stop" - ContainerOpRestart = "restart" - ContainerOpKill = "kill" - ContainerOpPause = "pause" - ContainerOpUnpause = "unpause" - ContainerOpRename = "rename" - ContainerOpRemove = "remove" - - ComposeOpStop = "stop" - ComposeOpRestart = "restart" - ComposeOpRemove = "remove" - - DaemonJsonPath = "/etc/docker/daemon.json" -) diff --git a/backend/constant/dir.go b/backend/constant/dir.go deleted file mode 100644 index addcf40be..000000000 --- a/backend/constant/dir.go +++ /dev/null @@ -1,21 +0,0 @@ -package constant - -import ( - "path" - - "github.com/1Panel-dev/1Panel/backend/global" -) - -var ( - DataDir = global.CONF.System.DataDir - ResourceDir = path.Join(DataDir, "resource") - AppResourceDir = path.Join(ResourceDir, "apps") - AppInstallDir = path.Join(DataDir, "apps") - LocalAppResourceDir = path.Join(AppResourceDir, "local") - LocalAppInstallDir = path.Join(AppInstallDir, "local") - RemoteAppResourceDir = path.Join(AppResourceDir, "remote") - RuntimeDir = path.Join(DataDir, "runtime") - RecycleBinDir = "/.1panel_clash" - SSLLogDir = path.Join(global.CONF.System.DataDir, "log", "ssl") - LogDir = path.Join(global.CONF.System.DataDir, "log") -) diff --git a/backend/constant/errs.go b/backend/constant/errs.go deleted file mode 100644 index 411387d3d..000000000 --- a/backend/constant/errs.go +++ /dev/null @@ -1,164 +0,0 @@ -package constant - -import ( - "errors" -) - -const ( - CodeSuccess = 200 - CodeErrBadRequest = 400 - CodeErrUnauthorized = 401 - CodeErrNotFound = 404 - CodeAuth = 406 - CodeGlobalLoading = 407 - CodeErrInternalServer = 500 - - CodeErrIP = 310 - CodeErrDomain = 311 - CodeErrEntrance = 312 - CodePasswordExpired = 313 - - CodeErrXpack = 410 -) - -// internal -var ( - ErrCaptchaCode = errors.New("ErrCaptchaCode") - ErrAuth = errors.New("ErrAuth") - ErrRecordExist = errors.New("ErrRecordExist") - ErrRecordNotFound = errors.New("ErrRecordNotFound") - ErrStructTransform = errors.New("ErrStructTransform") - ErrInitialPassword = errors.New("ErrInitialPassword") - ErrNotSupportType = errors.New("ErrNotSupportType") - ErrInvalidParams = errors.New("ErrInvalidParams") - - ErrTokenParse = errors.New("ErrTokenParse") -) - -// api -var ( - ErrTypeInternalServer = "ErrInternalServer" - ErrTypeInvalidParams = "ErrInvalidParams" - ErrTypeNotLogin = "ErrNotLogin" - ErrTypePasswordExpired = "ErrPasswordExpired" - ErrNameIsExist = "ErrNameIsExist" - ErrDemoEnvironment = "ErrDemoEnvironment" - ErrCmdIllegal = "ErrCmdIllegal" - ErrXpackNotFound = "ErrXpackNotFound" - ErrXpackNotActive = "ErrXpackNotActive" - ErrXpackOutOfDate = "ErrXpackOutOfDate" -) - -// app -var ( - ErrPortInUsed = "ErrPortInUsed" - ErrAppLimit = "ErrAppLimit" - ErrFileCanNotRead = "ErrFileCanNotRead" - ErrNotInstall = "ErrNotInstall" - ErrPortInOtherApp = "ErrPortInOtherApp" - ErrDbUserNotValid = "ErrDbUserNotValid" - ErrUpdateBuWebsite = "ErrUpdateBuWebsite" - Err1PanelNetworkFailed = "Err1PanelNetworkFailed" - ErrCmdTimeout = "ErrCmdTimeout" - ErrFileParse = "ErrFileParse" - ErrInstallDirNotFound = "ErrInstallDirNotFound" - ErrContainerName = "ErrContainerName" - ErrAppNameExist = "ErrAppNameExist" - ErrFileNotFound = "ErrFileNotFound" - ErrFileParseApp = "ErrFileParseApp" - ErrAppParamKey = "ErrAppParamKey" -) - -// website -var ( - ErrDomainIsExist = "ErrDomainIsExist" - ErrAliasIsExist = "ErrAliasIsExist" - ErrGroupIsUsed = "ErrGroupIsUsed" - ErrUsernameIsExist = "ErrUsernameIsExist" - ErrUsernameIsNotExist = "ErrUsernameIsNotExist" - ErrBackupMatch = "ErrBackupMatch" - ErrBackupExist = "ErrBackupExist" - ErrDomainIsUsed = "ErrDomainIsUsed" -) - -// ssl -var ( - ErrSSLCannotDelete = "ErrSSLCannotDelete" - ErrAccountCannotDelete = "ErrAccountCannotDelete" - ErrSSLApply = "ErrSSLApply" - ErrEmailIsExist = "ErrEmailIsExist" - ErrEabKidOrEabHmacKeyCannotBlank = "ErrEabKidOrEabHmacKeyCannotBlank" -) - -// file -var ( - ErrPathNotFound = "ErrPathNotFound" - ErrMovePathFailed = "ErrMovePathFailed" - ErrLinkPathNotFound = "ErrLinkPathNotFound" - ErrFileIsExist = "ErrFileIsExist" - ErrFileUpload = "ErrFileUpload" - ErrFileDownloadDir = "ErrFileDownloadDir" - ErrCmdNotFound = "ErrCmdNotFound" - ErrFavoriteExist = "ErrFavoriteExist" -) - -// mysql -var ( - ErrUserIsExist = "ErrUserIsExist" - ErrDatabaseIsExist = "ErrDatabaseIsExist" - ErrExecTimeOut = "ErrExecTimeOut" - ErrRemoteExist = "ErrRemoteExist" - ErrLocalExist = "ErrLocalExist" -) - -// redis -var ( - ErrTypeOfRedis = "ErrTypeOfRedis" -) - -// container -var ( - ErrInUsed = "ErrInUsed" - ErrObjectInUsed = "ErrObjectInUsed" - ErrPortRules = "ErrPortRules" - ErrPgImagePull = "ErrPgImagePull" -) - -// runtime -var ( - ErrDirNotFound = "ErrDirNotFound" - ErrFileNotExist = "ErrFileNotExist" - ErrImageBuildErr = "ErrImageBuildErr" - ErrImageExist = "ErrImageExist" - ErrDelWithWebsite = "ErrDelWithWebsite" - ErrRuntimeStart = "ErrRuntimeStart" - ErrPackageJsonNotFound = "ErrPackageJsonNotFound" - ErrScriptsNotFound = "ErrScriptsNotFound" -) - -var ( - ErrBackupInUsed = "ErrBackupInUsed" - ErrOSSConn = "ErrOSSConn" - ErrEntrance = "ErrEntrance" -) - -var ( - ErrFirewall = "ErrFirewall" -) - -// cronjob -var ( - ErrBashExecute = "ErrBashExecute" -) - -var ( - ErrNotExistUser = "ErrNotExistUser" -) - -// license -var ( - ErrLicense = "ErrLicense" - ErrLicenseCheck = "ErrLicenseCheck" - ErrLicenseSave = "ErrLicenseSave" - ErrLicenseSync = "ErrLicenseSync" -) diff --git a/backend/constant/host_tool.go b/backend/constant/host_tool.go deleted file mode 100644 index 60f483b02..000000000 --- a/backend/constant/host_tool.go +++ /dev/null @@ -1,8 +0,0 @@ -package constant - -const ( - Supervisord = "supervisord" - Supervisor = "supervisor" - SupervisorConfigPath = "SupervisorConfigPath" - SupervisorServiceName = "SupervisorServiceName" -) diff --git a/backend/constant/nginx.go b/backend/constant/nginx.go deleted file mode 100644 index 93b46b868..000000000 --- a/backend/constant/nginx.go +++ /dev/null @@ -1,15 +0,0 @@ -package constant - -const ( - NginxScopeServer = "server" - NginxScopeHttp = "http" - NginxScopeOut = "out" - - NginxReload = "reload" - NginxCheck = "check" - NginxRestart = "restart" - - ConfigNew = "add" - ConfigUpdate = "update" - ConfigDel = "delete" -) diff --git a/backend/constant/runtime.go b/backend/constant/runtime.go deleted file mode 100644 index 55a524983..000000000 --- a/backend/constant/runtime.go +++ /dev/null @@ -1,35 +0,0 @@ -package constant - -const ( - ResourceLocal = "local" - ResourceAppstore = "appstore" - - RuntimeNormal = "normal" - RuntimeError = "error" - RuntimeBuildIng = "building" - RuntimeStarting = "starting" - RuntimeRunning = "running" - RuntimeReCreating = "recreating" - RuntimeStopped = "stopped" - RuntimeUnhealthy = "unhealthy" - RuntimeCreating = "creating" - - RuntimePHP = "php" - RuntimeNode = "node" - RuntimeJava = "java" - RuntimeGo = "go" - - RuntimeProxyUnix = "unix" - RuntimeProxyTcp = "tcp" - - RuntimeUp = "up" - RuntimeDown = "down" - RuntimeRestart = "restart" - - RuntimeInstall = "install" - RuntimeUninstall = "uninstall" - RuntimeUpdate = "update" - - RuntimeNpm = "npm" - RuntimeYarn = "yarn" -) diff --git a/backend/constant/session.go b/backend/constant/session.go deleted file mode 100644 index 94fdf462d..000000000 --- a/backend/constant/session.go +++ /dev/null @@ -1,13 +0,0 @@ -package constant - -const ( - AuthMethodSession = "session" - SessionName = "psession" - - AuthMethodJWT = "jwt" - JWTHeaderName = "PanelAuthorization" - JWTBufferTime = 3600 - JWTIssuer = "1Panel" - - PasswordExpiredName = "expired" -) diff --git a/backend/constant/status.go b/backend/constant/status.go deleted file mode 100644 index 161b50e00..000000000 --- a/backend/constant/status.go +++ /dev/null @@ -1,16 +0,0 @@ -package constant - -const ( - StatusRunning = "Running" - StatusDone = "Done" - StatusWaiting = "Waiting" - StatusSuccess = "Success" - StatusFailed = "Failed" - StatusUploading = "Uploading" - StatusEnable = "Enable" - StatusDisable = "Disable" - StatusNone = "None" - - OrderDesc = "descending" - OrderAsc = "ascending" -) diff --git a/backend/constant/website.go b/backend/constant/website.go deleted file mode 100644 index a236fc6a8..000000000 --- a/backend/constant/website.go +++ /dev/null @@ -1,51 +0,0 @@ -package constant - -const ( - WebRunning = "Running" - WebStopped = "Stopped" - - ProtocolHTTP = "HTTP" - ProtocolHTTPS = "HTTPS" - - NewApp = "new" - InstalledApp = "installed" - - Deployment = "deployment" - Static = "static" - Proxy = "proxy" - Runtime = "runtime" - - SSLExisted = "existed" - SSLAuto = "auto" - SSLManual = "manual" - - DNSAccount = "dnsAccount" - DnsManual = "dnsManual" - Http = "http" - Manual = "manual" - SelfSigned = "selfSigned" - - StartWeb = "start" - StopWeb = "stop" - - HTTPSOnly = "HTTPSOnly" - HTTPAlso = "HTTPAlso" - HTTPToHTTPS = "HTTPToHTTPS" - - GetLog = "get" - DisableLog = "disable" - EnableLog = "enable" - DeleteLog = "delete" - - AccessLog = "access.log" - ErrorLog = "error.log" - - ConfigPHP = "php" - ConfigFPM = "fpm" - - SSLInit = "init" - SSLError = "error" - SSLReady = "ready" - SSLApply = "applying" - SSLApplyError = "applyError" -) diff --git a/backend/cron/cron.go b/backend/cron/cron.go deleted file mode 100644 index ec9f6d9d5..000000000 --- a/backend/cron/cron.go +++ /dev/null @@ -1,102 +0,0 @@ -package cron - -import ( - "fmt" - mathRand "math/rand" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/app/repo" - "github.com/1Panel-dev/1Panel/backend/app/service" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/cron/job" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/1Panel-dev/1Panel/backend/utils/ntp" - "github.com/robfig/cron/v3" -) - -func Run() { - nyc, _ := time.LoadLocation(common.LoadTimeZone()) - global.Cron = cron.New(cron.WithLocation(nyc), cron.WithChain(cron.Recover(cron.DefaultLogger)), cron.WithChain(cron.DelayIfStillRunning(cron.DefaultLogger))) - - var ( - interval model.Setting - status model.Setting - ) - syncBeforeStart() - if err := global.DB.Where("key = ?", "MonitorStatus").Find(&status).Error; err != nil { - global.LOG.Errorf("load monitor status from db failed, err: %v", err) - } - if status.Value == "enable" { - if err := global.DB.Where("key = ?", "MonitorInterval").Find(&interval).Error; err != nil { - global.LOG.Errorf("load monitor interval from db failed, err: %v", err) - } - if err := service.StartMonitor(false, interval.Value); err != nil { - global.LOG.Errorf("can not add monitor corn job: %s", err.Error()) - } - } - - if _, err := global.Cron.AddJob("@daily", job.NewWebsiteJob()); err != nil { - global.LOG.Errorf("can not add website corn job: %s", err.Error()) - } - if _, err := global.Cron.AddJob("@daily", job.NewSSLJob()); err != nil { - global.LOG.Errorf("can not add ssl corn job: %s", err.Error()) - } - if _, err := global.Cron.AddJob(fmt.Sprintf("%v %v * * *", mathRand.Intn(60), mathRand.Intn(3)), job.NewAppStoreJob()); err != nil { - global.LOG.Errorf("can not add appstore corn job: %s", err.Error()) - } - if _, err := global.Cron.AddJob("@daily", job.NewCacheJob()); err != nil { - global.LOG.Errorf("can not add cache corn job: %s", err.Error()) - } - - var backup model.BackupAccount - _ = global.DB.Where("type = ?", "OneDrive").Find(&backup).Error - if backup.ID != 0 { - service.StartRefreshOneDriveToken() - } - global.Cron.Start() - - var cronJobs []model.Cronjob - if err := global.DB.Where("status = ?", constant.StatusEnable).Find(&cronJobs).Error; err != nil { - global.LOG.Errorf("start my cronjob failed, err: %v", err) - } - if err := global.DB.Model(&model.JobRecords{}). - Where("status = ?", constant.StatusRunning). - Updates(map[string]interface{}{ - "status": constant.StatusFailed, - "message": "Task Cancel", - "records": "errHandle", - }).Error; err != nil { - global.LOG.Errorf("start my cronjob failed, err: %v", err) - } - for i := 0; i < len(cronJobs); i++ { - entryIDs, err := service.NewICronjobService().StartJob(&cronJobs[i], false) - if err != nil { - global.LOG.Errorf("start %s job %s failed, err: %v", cronJobs[i].Type, cronJobs[i].Name, err) - } - if err := repo.NewICronjobRepo().Update(cronJobs[i].ID, map[string]interface{}{"entry_ids": entryIDs}); err != nil { - global.LOG.Errorf("update cronjob %s %s failed, err: %v", cronJobs[i].Type, cronJobs[i].Name, err) - } - } -} - -func syncBeforeStart() { - var ntpSite model.Setting - if err := global.DB.Where("key = ?", "NtpSite").Find(&ntpSite).Error; err != nil { - global.LOG.Errorf("load ntp serve from db failed, err: %v", err) - } - if len(ntpSite.Value) == 0 { - ntpSite.Value = "pool.ntp.org" - } - ntime, err := ntp.GetRemoteTime(ntpSite.Value) - if err != nil { - global.LOG.Errorf("load remote time with [%s] failed, err: %v", ntpSite.Value, err) - return - } - ts := ntime.Format(constant.DateTimeLayout) - if err := ntp.UpdateSystemTime(ts); err != nil { - global.LOG.Errorf("failed to synchronize system time with [%s], err: %v", ntpSite.Value, err) - } - global.LOG.Debugf("synchronize system time with [%s] successful!", ntpSite.Value) -} diff --git a/backend/cron/job/app.go b/backend/cron/job/app.go deleted file mode 100644 index 92142f021..000000000 --- a/backend/cron/job/app.go +++ /dev/null @@ -1,20 +0,0 @@ -package job - -import ( - "github.com/1Panel-dev/1Panel/backend/app/service" - "github.com/1Panel-dev/1Panel/backend/global" -) - -type app struct{} - -func NewAppStoreJob() *app { - return &app{} -} - -func (a *app) Run() { - global.LOG.Info("AppStore scheduled task in progress ...") - if err := service.NewIAppService().SyncAppListFromRemote(); err != nil { - global.LOG.Errorf("AppStore sync failed %s", err.Error()) - } - global.LOG.Info("AppStore scheduled task has completed") -} diff --git a/backend/cron/job/cache.go b/backend/cron/job/cache.go deleted file mode 100644 index 2fb3cd643..000000000 --- a/backend/cron/job/cache.go +++ /dev/null @@ -1,26 +0,0 @@ -package job - -import ( - "github.com/1Panel-dev/1Panel/backend/global" - "time" -) - -type Cache struct{} - -func NewCacheJob() *Cache { - return &Cache{} -} - -func (c *Cache) Run() { - global.LOG.Info("run cache gc start ...") - ticker := time.NewTicker(5 * time.Minute) - defer ticker.Stop() - for range ticker.C { - again: - err := global.CacheDb.RunValueLogGC(0.7) - if err == nil { - goto again - } - } - global.LOG.Info("run cache gc end ...") -} diff --git a/backend/cron/job/ssl.go b/backend/cron/job/ssl.go deleted file mode 100644 index 374d45664..000000000 --- a/backend/cron/job/ssl.go +++ /dev/null @@ -1,76 +0,0 @@ -package job - -import ( - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/app/repo" - "github.com/1Panel-dev/1Panel/backend/app/service" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/1Panel-dev/1Panel/backend/utils/files" - "path" - "strings" - "time" -) - -type ssl struct { -} - -func NewSSLJob() *ssl { - return &ssl{} -} - -func (ssl *ssl) Run() { - systemSSLEnable, sslID := service.GetSystemSSL() - sslRepo := repo.NewISSLRepo() - sslService := service.NewIWebsiteSSLService() - sslList, _ := sslRepo.List() - nyc, _ := time.LoadLocation(common.LoadTimeZone()) - global.LOG.Info("The scheduled certificate update task is currently in progress ...") - now := time.Now().Add(10 * time.Second) - for _, s := range sslList { - if !s.AutoRenew || s.Provider == "manual" || s.Provider == "dnsManual" || s.Status == "applying" { - continue - } - expireDate := s.ExpireDate.In(nyc) - sub := expireDate.Sub(now) - if sub.Hours() < 720 { - global.LOG.Infof("Update the SSL certificate for the [%s] domain", s.PrimaryDomain) - if s.Provider == constant.SelfSigned { - caService := service.NewIWebsiteCAService() - if _, err := caService.ObtainSSL(request.WebsiteCAObtain{ - ID: s.CaID, - SSLID: s.ID, - Renew: true, - Unit: "year", - Time: 10, - }); err != nil { - global.LOG.Errorf("Failed to update the SSL certificate for the [%s] domain , err:%s", s.PrimaryDomain, err.Error()) - continue - } - } else { - if err := sslService.ObtainSSL(request.WebsiteSSLApply{ - ID: s.ID, - }); err != nil { - global.LOG.Errorf("Failed to update the SSL certificate for the [%s] domain , err:%s", s.PrimaryDomain, err.Error()) - continue - } - } - if systemSSLEnable && sslID == s.ID { - websiteSSL, _ := sslRepo.GetFirst(repo.NewCommonRepo().WithByID(s.ID)) - fileOp := files.NewFileOp() - secretDir := path.Join(global.CONF.System.BaseDir, "1panel/secret") - if err := fileOp.WriteFile(path.Join(secretDir, "server.crt"), strings.NewReader(websiteSSL.Pem), 0600); err != nil { - global.LOG.Errorf("Failed to update the SSL certificate File for 1Panel System domain [%s] , err:%s", s.PrimaryDomain, err.Error()) - continue - } - if err := fileOp.WriteFile(path.Join(secretDir, "server.key"), strings.NewReader(websiteSSL.PrivateKey), 0600); err != nil { - global.LOG.Errorf("Failed to update the SSL certificate for 1Panel System domain [%s] , err:%s", s.PrimaryDomain, err.Error()) - continue - } - } - global.LOG.Infof("The SSL certificate for the [%s] domain has been successfully updated", s.PrimaryDomain) - } - } - global.LOG.Info("The scheduled certificate update task has completed") -} diff --git a/backend/cron/job/website.go b/backend/cron/job/website.go deleted file mode 100644 index 4eb91373b..000000000 --- a/backend/cron/job/website.go +++ /dev/null @@ -1,62 +0,0 @@ -package job - -import ( - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/app/repo" - "github.com/1Panel-dev/1Panel/backend/app/service" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/common" - "sync" - "time" -) - -type website struct{} - -func NewWebsiteJob() *website { - return &website{} -} - -func (w *website) Run() { - nyc, _ := time.LoadLocation(common.LoadTimeZone()) - websites, _ := repo.NewIWebsiteRepo().List() - global.LOG.Info("Website scheduled task in progress ...") - now := time.Now().Add(10 * time.Minute) - if len(websites) > 0 { - neverExpireDate, _ := time.Parse(constant.DateLayout, constant.DefaultDate) - var wg sync.WaitGroup - for _, site := range websites { - if site.Status != constant.WebRunning || neverExpireDate.Equal(site.ExpireDate) { - continue - } - expireDate, err := time.ParseInLocation(constant.DateLayout, site.ExpireDate.Format(constant.DateLayout), nyc) - if err != nil { - global.LOG.Errorf("time parse err %v", err) - continue - } - if expireDate.Before(now) { - wg.Add(1) - go func(ws model.Website) { - stopWebsite(ws.ID, ws.PrimaryDomain, &wg) - }(site) - } - } - wg.Wait() - } - global.LOG.Info("Website scheduled task has completed") -} - -func stopWebsite(websiteId uint, websiteName string, wg *sync.WaitGroup) { - websiteService := service.NewIWebsiteService() - req := request.WebsiteOp{ - ID: websiteId, - Operate: constant.StopWeb, - } - if err := websiteService.OpWebsite(req); err != nil { - global.LOG.Errorf("Website [%s] seop failed err %v", websiteName, err) - } else { - global.LOG.Infof("Website [%s] stopped successfully", websiteName) - } - wg.Done() -} diff --git a/backend/global/global.go b/backend/global/global.go deleted file mode 100644 index cf92a8fc0..000000000 --- a/backend/global/global.go +++ /dev/null @@ -1,30 +0,0 @@ -package global - -import ( - "github.com/1Panel-dev/1Panel/backend/configs" - "github.com/1Panel-dev/1Panel/backend/init/cache/badger_db" - "github.com/dgraph-io/badger/v4" - "github.com/go-playground/validator/v10" - "github.com/nicksnyder/go-i18n/v2/i18n" - "github.com/robfig/cron/v3" - "github.com/sirupsen/logrus" - "github.com/spf13/viper" - "gorm.io/gorm" -) - -var ( - DB *gorm.DB - MonitorDB *gorm.DB - LOG *logrus.Logger - CONF configs.ServerConfig - VALID *validator.Validate - CACHE *badger_db.Cache - CacheDb *badger.DB - Viper *viper.Viper - - Cron *cron.Cron - MonitorCronID cron.EntryID - OneDriveCronID cron.EntryID - - I18n *i18n.Localizer -) diff --git a/backend/i18n/i18n.go b/backend/i18n/i18n.go deleted file mode 100644 index 2cc7d4802..000000000 --- a/backend/i18n/i18n.go +++ /dev/null @@ -1,130 +0,0 @@ -package i18n - -import ( - "embed" - "github.com/1Panel-dev/1Panel/backend/global" - "strings" - - "github.com/gin-gonic/gin" - "github.com/nicksnyder/go-i18n/v2/i18n" - "golang.org/x/text/language" - "gopkg.in/yaml.v3" -) - -func GetMsgWithMap(key string, maps map[string]interface{}) string { - var content string - if maps == nil { - content, _ = global.I18n.Localize(&i18n.LocalizeConfig{ - MessageID: key, - }) - } else { - content, _ = global.I18n.Localize(&i18n.LocalizeConfig{ - MessageID: key, - TemplateData: maps, - }) - } - content = strings.ReplaceAll(content, ": ", "") - if content == "" { - return key - } else { - return content - } -} - -func GetMsgWithName(key string, name string, err error) string { - var ( - content string - dataMap = make(map[string]interface{}) - ) - dataMap["name"] = name - if err != nil { - dataMap["err"] = err.Error() - } - content, _ = global.I18n.Localize(&i18n.LocalizeConfig{ - MessageID: key, - TemplateData: dataMap, - }) - content = strings.ReplaceAll(content, "", "") - if content == "" { - return key - } else { - return content - } -} - -func GetErrMsg(key string, maps map[string]interface{}) string { - var content string - if maps == nil { - content, _ = global.I18n.Localize(&i18n.LocalizeConfig{ - MessageID: key, - }) - } else { - content, _ = global.I18n.Localize(&i18n.LocalizeConfig{ - MessageID: key, - TemplateData: maps, - }) - } - return content -} - -func GetMsgByKey(key string) string { - content, _ := global.I18n.Localize(&i18n.LocalizeConfig{ - MessageID: key, - }) - return content -} - -func GetWithName(key string, name string) string { - var ( - dataMap = make(map[string]interface{}) - ) - dataMap["name"] = name - content, _ := global.I18n.Localize(&i18n.LocalizeConfig{ - MessageID: key, - TemplateData: dataMap, - }) - return content -} - -func GetWithMap(key string, dataMap map[string]string) string { - content, _ := global.I18n.Localize(&i18n.LocalizeConfig{ - MessageID: key, - TemplateData: dataMap, - }) - return content -} - -func GetWithNameAndErr(key string, name string, err error) string { - var ( - dataMap = make(map[string]interface{}) - ) - dataMap["name"] = name - dataMap["err"] = err.Error() - content, _ := global.I18n.Localize(&i18n.LocalizeConfig{ - MessageID: key, - TemplateData: dataMap, - }) - return content -} - -//go:embed lang/* -var fs embed.FS -var bundle *i18n.Bundle - -func UseI18n() gin.HandlerFunc { - return func(context *gin.Context) { - lang := context.GetHeader("Accept-Language") - if lang == "" { - lang = "zh" - } - global.I18n = i18n.NewLocalizer(bundle, lang) - } -} - -func Init() { - bundle = i18n.NewBundle(language.Chinese) - bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal) - _, _ = bundle.LoadMessageFileFS(fs, "lang/zh.yaml") - _, _ = bundle.LoadMessageFileFS(fs, "lang/en.yaml") - _, _ = bundle.LoadMessageFileFS(fs, "lang/zh-Hant.yaml") -} diff --git a/backend/i18n/lang/en.yaml b/backend/i18n/lang/en.yaml deleted file mode 100644 index 69b99e474..000000000 --- a/backend/i18n/lang/en.yaml +++ /dev/null @@ -1,207 +0,0 @@ -ErrInvalidParams: "Request parameter error: {{ .detail }}" -ErrTokenParse: "Token generation error: {{ .detail }}" -ErrInitialPassword: "Initial password error" -ErrInternalServer: "Service internal error: {{ .detail }}" -ErrRecordExist: "Record already exists" -ErrRecordNotFound: "Records not found" -ErrStructTransform: "Type conversion failure: {{ .detail }}" -ErrNotLogin: "User is not Login: {{ .detail }}" -ErrPasswordExpired: "The current password has expired: {{ .detail }}" -ErrNotSupportType: "The system does not support the current type: {{ .detail }}" - -#common -ErrNameIsExist: "Name is already exist" -ErrDemoEnvironment: "Demo server, prohibit this operation!" -ErrCmdTimeout: "Command execution timed out!" -ErrCmdIllegal: "The command contains illegal characters. Please modify and try again!" -ErrPortExist: '{{ .port }} port is already occupied by {{ .type }} [{{ .name }}]' -TYPE_APP: "Application" -TYPE_RUNTIME: "Runtime environment" -TYPE_DOMAIN: "Domain name" -ErrTypePort: 'Port {{ .name }} format error' -ErrTypePortRange: 'Port range needs to be between 1-65535' -Success: "Success" -Failed: "Failed" -SystemRestart: "System restart causes task interruption" - -#app -ErrPortInUsed: "{{ .detail }} port already in use" -ErrAppLimit: "App exceeds install limit" -ErrAppRequired: "{{ .detail }} app is required" -ErrNotInstall: "App not installed" -ErrPortInOtherApp: "{{ .port }} port already in use by app {{ .apps }}" -ErrDbUserNotValid: "Stock database, username and password do not match!" -ErrDockerComposeNotValid: "docker-compose file format error!" -ErrUpdateBuWebsite: 'The application was updated successfully, but the modification of the website configuration file failed, please check the configuration!' -Err1PanelNetworkFailed: 'Default container network creation failed! {{ .detail }}' -ErrFileParse: 'Application docker-compose file parsing failed!' -ErrInstallDirNotFound: 'installation directory does not exist' -AppStoreIsUpToDate: 'The app store is already up to date!' -LocalAppVersionNull: 'The {{.name}} app is not synced to version! Could not add to application list' -LocalAppVersionErr: '{{.name}} failed to sync version {{.version}}! {{.err}}' -ErrFileNotFound: '{{.name}} file does not exist' -ErrFileParseApp: 'Failed to parse {{.name}} file {{.err}}' -ErrAppDirNull: 'version folder does not exist' -LocalAppErr: "App {{.name}} sync failed! {{.err}}" -ErrContainerName: "ContainerName is already exist" -ErrAppSystemRestart: "1Panel restart causes the task to terminate" -ErrCreateHttpClient: "Failed to create HTTP request {{.err}}" -ErrHttpReqTimeOut: "Request timed out {{.err}}" -ErrHttpReqFailed: "Request failed {{.err}}" -ErrHttpReqNotFound: "The file does not exist" -ErrNoSuchHost: "Network connection failed" -ErrImagePullTimeOut: 'Image pull timeout' -ErrContainerNotFound: '{{ .name }} container does not exist' -ErrContainerMsg: '{{ .name }} container is abnormal, please check the log on the container page for details' -ErrAppBackup: '{{ .name }} application backup failed err {{.err}}' -ErrImagePull: '{{ .name }} image pull failed err {{.err}}' -ErrVersionTooLow: 'The current 1Panel version is too low to update the app store, please upgrade the version' -ErrAppNameExist: 'App name is already exist' -AppStoreIsSyncing: 'The App Store is syncing, please try again later' -ErrGetCompose: "Failed to obtain docker-compose.yml file! {{ .detail }}" -ErrAppWarn: "Abnormal status, please check the log" -ErrAppParamKey: "Parameter {{ .name }} field exception" -ErrAppUpgrade: "Failed to upgrade application {{ .name }} {{ .err }}" -AppRecover: "App {{ .name }} rolled back " -PullImageStart: "Start pulling image {{ .name }}" -PullImageSuccess: "Image pulled successfully" -UpgradeAppStart: "Start upgrading application {{ .name }}" -UpgradeAppSuccess: "App {{ .name }} upgraded successfully" - -#file -ErrFileCanNotRead: "File can not read" -ErrFileToLarge: "file is too large" -ErrPathNotFound: "Path is not found" -ErrMovePathFailed: "The target path cannot contain the original path!" -ErrLinkPathNotFound: "Target path does not exist!" -ErrFileIsExist: "File or directory already exists!" -ErrFileUpload: "Failed to upload file {{.name}} {{.detail}}" -ErrFileDownloadDir: "Download folder not supported" -ErrCmdNotFound: "{{ .name}} command does not exist, please install this command on the host first" -ErrSourcePathNotFound: "Source directory does not exist" -ErrFavoriteExist: "This path has been collected" -ErrInvalidChar: "Illegal characters are prohibited" - -#website -ErrDomainIsExist: "Domain is already exist" -ErrAliasIsExist: "Alias is already exist" -ErrAppDelete: 'Other Website use this App' -ErrGroupIsUsed: 'The group is in use and cannot be deleted' -ErrBackupMatch: 'the backup file does not match the current partial data of the website: {{ .detail}}' -ErrBackupExist: 'the backup file corresponds to a portion of the original data that does not exist: {{ .detail}}' -ErrPHPResource: 'The local runtime does not support switching!' -ErrPathPermission: 'A folder with non-1000:1000 permissions was detected in the index directory, which may cause an Access denied error when accessing the website. Please click the save button above' -ErrDomainIsUsed: "Domain is already used by website {{ .name }}" -ErrDomainFormat: "{{ .name }} domain format error" -ErrDefaultAlias: "default is a reserved code name, please use another code name" - -#ssl -ErrSSLCannotDelete: "The certificate {{ .name }} is being used by the website and cannot be removed" -ErrAccountCannotDelete: "The certificate associated with the account cannot be deleted" -ErrSSLApply: "The certificate continues to be signed successfully, but openresty reload fails, please check the configuration!" -ErrEmailIsExist: 'Email is already exist' -ErrSSLKeyNotFound: 'The private key file does not exist' -ErrSSLCertificateNotFound: 'The certificate file does not exist' -ErrSSLKeyFormat: 'Private key file verification error' -ErrSSLCertificateFormat: 'Certificate file format error, please use pem format' -ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid or EabHmacKey cannot be empty' -ErrOpenrestyNotFound: 'Http mode requires Openresty to be installed first' -ApplySSLStart: 'Start applying for certificate, domain name [{{ .domain }}] application method [{{ .type }}] ' -dnsAccount: "DNS Automatic" -dnsManual: "DNS Manual" -http: "HTTP" -ApplySSLFailed: 'Application for [{{ .domain }}] certificate failed, {{.detail}} ' -ApplySSLSuccess: 'Application for [{{ .domain }}] certificate successful! ! ' -DNSAccountName: 'DNS account [{{ .name }}] manufacturer [{{.type}}]' -PushDirLog: 'Certificate pushed to directory [{{ .path }}] {{ .status }}' -ErrDeleteCAWithSSL: "There is an issued certificate under the current organization and cannot be deleted" -ErrDeleteWithPanelSSL: "Panel SSL configuration uses this certificate and cannot be deleted" -ErrDefaultCA: "The default organization cannot be deleted" -ApplyWebSiteSSLLog: "Start updating {{ .name }} website certificate" -ErrUpdateWebsiteSSL: "{{ .name }} website failed to update certificate: {{ .err }}" -ApplyWebSiteSSLSuccess: "Update website certificate successfully" - -#mysql -ErrUserIsExist: "The current user already exists. Please enter a new user" -ErrDatabaseIsExist: "The current database already exists. Please enter a new database" -ErrExecTimeOut: "SQL execution timed out, please check the database" -ErrRemoteExist: "The remote database already exists with that name, please modify it and try again" -ErrLocalExist: "The local database already exists with that name, please modify it and try again" - -#redis -ErrTypeOfRedis: "The recovery file type does not match the current persistence mode. Modify the file type and try again" - -#container -ErrInUsed: "{{ .detail }} is in use and cannot be deleted" -ErrObjectInUsed: "This object is in use and cannot be deleted" -ErrPortRules: "The number of ports does not match, please re-enter!" -ErrPgImagePull: "Image pull timeout. Please configure image acceleration or manually pull the postgres:16.0-alpine image and try again" - -#runtime -ErrDirNotFound: "The build folder does not exist! Please check file integrity!" -ErrFileNotExist: "{{ .detail }} file does not exist! Please check source file integrity!" -ErrImageBuildErr: "Image build failed" -ErrImageExist: "Image is already exist!" -ErrDelWithWebsite: "The operating environment has been associated with a website and cannot be deleted" -ErrRuntimeStart: "Failed to start" -ErrPackageJsonNotFound: "package.json file does not exist" -ErrScriptsNotFound: "No scripts configuration item was found in package.json" -ErrContainerNameNotFound: "Unable to get container name, please check .env file" -ErrNodeModulesNotFound: "The node_modules folder does not exist! Please edit the running environment or wait for the running environment to start successfully" - -#setting -ErrBackupInUsed: "The backup account is already being used in a cronjob and cannot be deleted." -ErrBackupCheck: "Backup account test connection failed {{ .err}}" -ErrOSSConn: "Unable to retrieve the latest version, please check if the server can connect to the external network." -ErrEntrance: "Security entrance information error. Please check and try again!" - -#tool -ErrConfigNotFound: "Configuration file does not exist" -ErrConfigParse: "Configuration file format error" -ErrConfigIsNull: "The configuration file is not allowed to be empty" -ErrConfigDirNotFound: "The running directory does not exist" -ErrConfigAlreadyExist: "A configuration file with the same name already exists" -ErrUserFindErr: "Failed to find user {{ .name }} {{ .err }}" - -#ssh -ErrFirewall: "No firewalld or ufw service is detected. Please check and try again!" - -#cronjob -ErrBashExecute: "Script execution error, please check the specific information in the task output text area." -ErrCutWebsiteLog: "{{ .name }} website log cutting failed, error {{ .err }}" -CutWebsiteLogSuccess: "{{ .name }} website log cut successfully, backup path {{ .path }}" - -#toolbox -ErrNotExistUser: "The current user does not exist. Please modify and retry!" -ErrBanAction: "Setting failed, the current {{ .name }} service is unavailable, please check and try again!" -ErrClamdscanNotFound: "The clamdscan command was not detected, please refer to the documentation to install it!" - -#waf -ErrScope: "Modification of this configuration is not supported" -ErrStateChange: "State modification failed" -ErrRuleExist: "Rule is Exist" -ErrRuleNotExist: "Rule is not Exist" -ErrParseIP: "IP format error" -ErrDefaultIP: "default is a reserved name, please change it to another name" -ErrGroupInUse: "The IP group is used by the black/white list and cannot be deleted" -ErrGroupExist: "IP group name already exists" -ErrIPRange: "Wrong IP range" -ErrIPExist: "IP is exit" - -#license -ErrLicense: "License format error, please check and try again!" -ErrLicenseCheck: "License verification failed, please check and try again!" -ErrLicenseSave: "Failed to save license information, error {{ .err }}, please try again!" -ErrLicenseSync: "Failed to sync license information, no license information detected in the database!" -ErrXpackNotFound: "This section is a professional edition feature, please import the license first in Panel Settings-License interface" -ErrXpackNotActive: "This section is a professional edition feature, please synchronize the license status first in Panel Settings-License interface" -ErrXpackOutOfDate: "The current license has expired, please re-import the license in Panel Settings-License interface" - -#task -TaskStart: "{{.name}} started [START]" -TaskEnd: "{{.name}} ended [COMPLETED]" -TaskFailed: "{{.name}} failed: {{.err}}" -TaskTimeout: "{{.name}} timed out" -TaskSuccess: "{{.name}} succeeded" -TaskRetry: "Start {{.name}} retry" -SubTaskStart: "Start {{.name}}" diff --git a/backend/i18n/lang/zh-Hant.yaml b/backend/i18n/lang/zh-Hant.yaml deleted file mode 100644 index 817f38e23..000000000 --- a/backend/i18n/lang/zh-Hant.yaml +++ /dev/null @@ -1,209 +0,0 @@ -ErrInvalidParams: "請求參數錯誤: {{ .detail }}" -ErrTokenParse: "Token 產生錯誤: {{ .detail }}" -ErrInitialPassword: "原密碼錯誤" -ErrInternalServer: "伺服器內部錯誤: {{ .detail }}" -ErrRecordExist: "記錄已存在" -ErrRecordNotFound: "記錄未找到" -ErrStructTransform: "類型轉換失敗: {{ .detail }}" -ErrNotLogin: "用戶未登入: {{ .detail }}" -ErrPasswordExpired: "當前密碼已過期: {{ .detail }}" -ErrNotSupportType: "系統暫不支持當前類型: {{ .detail }}" - -#common -ErrNameIsExist: "名稱已存在" -ErrDemoEnvironment: "演示伺服器,禁止此操作!" -ErrCmdTimeout: "指令執行超時!" -ErrCmdIllegal: "執行命令中存在不合法字符,請修改後重試!" -ErrPortExist: '{{ .port }} 埠已被 {{ .type }} [{{ .name }}] 佔用' -TYPE_APP: "應用" -TYPE_RUNTIME: "運作環境" -TYPE_DOMAIN: "網域名稱" -ErrTypePort: '埠 {{ .name }} 格式錯誤' -ErrTypePortRange: '連接埠範圍需要在 1-65535 之間' -Success: "成功" -Failed: "失敗" -SystemRestart: "系統重啟導致任務中斷" -ErrInvalidChar: "禁止使用非法字元" - -#app -ErrPortInUsed: "{{ .detail }} 端口已被佔用!" -ErrAppLimit: "應用超出安裝數量限制" -ErrAppRequired: "請先安裝 {{ .detail }} 應用" -ErrNotInstall: "應用未安裝" -ErrPortInOtherApp: "{{ .port }} 端口已被應用 {{ .apps }} 佔用!" -ErrDbUserNotValid: "儲存資料庫,用戶名密碼不匹配!" -ErrDockerComposeNotValid: "docker-compose 文件格式錯誤" -ErrUpdateBuWebsite: '應用更新成功,但是網站配置文件修改失敗,請檢查配置!' -Err1PanelNetworkFailed: '默認容器網絡創建失敗!{{ .detail }}' -ErrFileParse: '應用 docker-compose 文件解析失敗!' -ErrInstallDirNotFound: '安裝目錄不存在' -AppStoreIsUpToDate: '應用商店已經是最新版本' -LocalAppVersionNull: '{{.name}} 應用未同步到版本!無法添加到應用列表' -LocalAppVersionErr: '{{.name}} 同步版本 {{.version}} 失敗!{{.err}}' -ErrFileNotFound: '{{.name}} 文件不存在' -ErrFileParseApp: '{{.name}} 文件解析失敗 {{.err}}' -ErrAppDirNull: '版本資料夾不存在' -LocalAppErr: "應用 {{.name}} 同步失敗!{{.err}}" -ErrContainerName: "容器名稱已存在" -ErrAppSystemRestart: "1Panel 重啟導致任務中斷" -ErrCreateHttpClient: "創建HTTP請求失敗 {{.err}}" -ErrHttpReqTimeOut: "請求超時 {{.err}}" -ErrHttpReqFailed: "請求失敗 {{.err}}" -ErrHttpReqNotFound: "文件不存在" -ErrNoSuchHost: "網路連接失敗" -ErrImagePullTimeOut: "鏡像拉取超時" -ErrContainerNotFound: '{{ .name }} 容器不存在' -ErrContainerMsg: '{{ .name }} 容器異常,具體請在容器頁面查看日誌' -ErrAppBackup: '{{ .name }} 應用備份失敗 err {{.err}}' -ErrImagePull: '{{ .name }} 鏡像拉取失敗 err {{.err}}' -ErrVersionTooLow: '當前 1Panel 版本過低,無法更新應用商店,請升級版本之後操作' -ErrAppNameExist: '應用名稱已存在' -AppStoreIsSyncing: '應用程式商店正在同步中,請稍後再試' -ErrGetCompose: "docker-compose.yml 檔案取得失敗!{{ .detail }}" -ErrAppWarn: "狀態異常,請查看日誌" -ErrAppParamKey: "參數 {{ .name }} 欄位異常" -ErrAppUpgrade: "應用程式 {{ .name }} 升級失敗 {{ .err }}" -AppRecover: "應用程式 {{ .name }} 回滾 " -PullImageStart: "開始拉取鏡像 {{ .name }}" -PullImageSuccess: "鏡像拉取成功" -UpgradeAppStart: "開始升級應用程式 {{ .name }}" -UpgradeAppSuccess: "應用程式 {{ .name }} 升級成功" - -#file -ErrFileCanNotRead: "此文件不支持預覽" -ErrFileToLarge: "文件超過10M,無法打開" -ErrPathNotFound: "目錄不存在" -ErrMovePathFailed: "目標路徑不能包含原路徑!" -ErrLinkPathNotFound: "目標路徑不存在!" -ErrFileIsExist: "文件或文件夾已存在!" -ErrFileUpload: "{{ .name }} 上傳文件失敗 {{ .detail}}" -ErrFileDownloadDir: "不支持下載文件夾" -ErrCmdNotFound: "{{ .name}} 命令不存在,請先在宿主機安裝此命令" -ErrSourcePathNotFound: "源目錄不存在" -ErrFavoriteExist: "已收藏此路徑" - -#website -ErrDomainIsExist: "域名已存在" -ErrAliasIsExist: "代號已存在" -ErrAppDelete: '其他網站使用此應用,無法刪除' -ErrGroupIsUsed: '分組正在使用中,無法刪除' -ErrBackupMatch: '該備份文件與當前網站部分數據不匹配: {{ .detail}}' -ErrBackupExist: '該備份文件對應部分原數據不存在: {{ .detail}}' -ErrPHPResource: '本地運行環境不支持切換!' -ErrPathPermission: 'index 目錄下偵測到非 1000:1000 權限資料夾,可能導致網站存取 Access denied 錯誤,請點擊上方儲存按鈕' -ErrDomainIsUsed: "域名已被網站【{{ .name }}】使用" -ErrDomainFormat: "{{ .name }} 域名格式不正確" -ErrDefaultAlias: "default 為保留代號,請使用其他代號" - -#ssl -ErrSSLCannotDelete: "{{ .name }} 證書正在被網站使用,無法刪除" -ErrAccountCannotDelete: "帳號關聯證書,無法刪除" -ErrSSLApply: "證書續簽成功,openresty reload失敗,請檢查配置!" -ErrEmailIsExist: '郵箱已存在' -ErrSSLKeyNotFound: '私鑰文件不存在' -ErrSSLCertificateNotFound: '證書文件不存在' -ErrSSLKeyFormat: '私鑰文件校驗錯誤' -ErrSSLCertificateFormat: '證書文件格式錯誤,請使用 pem 格式' -ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid 或 EabHmacKey 不能為空' -ErrOpenrestyNotFound: 'Http 模式需要先安裝 Openresty' -ApplySSLStart: '開始申請憑證,網域 [{{ .domain }}] 申請方式 [{{ .type }}] ' -dnsAccount: "DNS 自動" -dnsManual: "DNS 手排" -http: "HTTP" -ApplySSLFailed: '申請 [{{ .domain }}] 憑證失敗, {{.detail}} ' -ApplySSLSuccess: '申請 [{{ .domain }}] 憑證成功! ! ' -DNSAccountName: 'DNS 帳號 [{{ .name }}] 廠商 [{{.type}}]' -PushDirLog: '憑證推送到目錄 [{{ .path }}] {{ .status }}' -ErrDeleteCAWithSSL: "目前機構下存在已簽發證書,無法刪除" -ErrDeleteWithPanelSSL: "面板 SSL 配置使用此證書,無法刪除" -ErrDefaultCA: "默認機構不能刪除" -ApplyWebSiteSSLLog: "開始更新 {{ .name }} 網站憑證" -ErrUpdateWebsiteSSL: "{{ .name }} 網站更新憑證失敗: {{ .err }}" -ApplyWebSiteSSLSuccess: "更新網站憑證成功" - - -#mysql -ErrUserIsExist: "當前用戶已存在,請重新輸入" -ErrDatabaseIsExist: "當前資料庫已存在,請重新輸入" -ErrExecTimeOut: "SQL 執行超時,請檢查數據庫" -ErrRemoteExist: "遠程數據庫已存在該名稱,請修改後重試" -ErrLocalExist: "本地數據庫已存在該名稱,請修改後重試" - -#redis -ErrTypeOfRedis: "恢復文件類型與當前持久化方式不符,請修改後重試" - -#container -ErrInUsed: "{{ .detail }} 正被使用,無法刪除" -ErrObjectInUsed: "該對象正被使用,無法刪除" -ErrPortRules: "端口數目不匹配,請重新輸入!" -ErrPgImagePull: "鏡像拉取超時,請配置鏡像加速或手動拉取 postgres:16.0-alpine 鏡像後重試" - -#runtime -ErrDirNotFound: "build 文件夾不存在!請檢查文件完整性!" -ErrFileNotExist: "{{ .detail }} 文件不存在!請檢查源文件完整性!" -ErrImageBuildErr: "鏡像 build 失敗" -ErrImageExist: "鏡像已存在!" -ErrDelWithWebsite: "運行環境已經關聯網站,無法刪除" -ErrRuntimeStart: "啟動失敗" -ErrPackageJsonNotFound: "package.json 文件不存在" -ErrScriptsNotFound: "沒有在 package.json 中找到 scripts 配置項" -ErrContainerNameNotFound: "無法取得容器名稱,請檢查 .env 文件" -ErrNodeModulesNotFound: "node_modules 文件夾不存在!請編輯運行環境或者等待運行環境啟動成功" - -#setting -ErrBackupInUsed: "該備份帳號已在計劃任務中使用,無法刪除" -ErrBackupCheck: "備份帳號測試連接失敗 {{ .err}}" -ErrOSSConn: "無法獲取最新版本,請確認伺服器是否能夠連接外部網路。" -ErrEntrance: "安全入口信息錯誤,請檢查後重試!" - -#tool -ErrConfigNotFound: "配置文件不存在" -ErrConfigParse: "配置文件格式有誤" -ErrConfigIsNull: "配置文件不允許為空" -ErrConfigDirNotFound: "運行目錄不存在" -ErrConfigAlreadyExist: "已存在同名配置文件" -ErrUserFindErr: "用戶 {{ .name }} 查找失敗 {{ .err }}" - -#ssh -ErrFirewall: "當前未檢測到系統 firewalld 或 ufw 服務,請檢查後重試!" - -#cronjob -ErrBashExecute: "腳本執行錯誤,請在任務輸出文本域中查看具體信息。" -ErrCutWebsiteLog: "{{ .name }} 網站日誌切割失敗,錯誤 {{ .err }}" -CutWebsiteLogSuccess: "{{ .name }} 網站日誌切割成功,備份路徑 {{ .path }}" - -#toolbox -ErrNotExistUser: "當前使用者不存在,請修改後重試!" -ErrBanAction: "設置失敗,當前 {{ .name }} 服務不可用,請檢查後重試!" -ErrClamdscanNotFound: "未偵測到 clamdscan 指令,請參考文件安裝!" - -#waf -ErrScope: "不支援修改此配置" -ErrStateChange: "狀態修改失敗" -ErrRuleExist: "規則已存在" -ErrRuleNotExist: "規則不存在" -ErrParseIP: "IP 格式錯誤" -ErrDefaultIP: "default 為保留名稱,請更換其他名稱" -ErrGroupInUse: "IP 群組被黑/白名單使用,無法刪除" -ErrGroupExist: "IP 群組名稱已存在" -ErrIPRange: "IP 範圍錯誤" -ErrIPExist: "IP 已存在" - - -#license -ErrLicense: "許可證格式錯誤,請檢查後重試!" -ErrLicenseCheck: "許可證校驗失敗,請檢查後重試!" -ErrLicenseSave: "許可證信息保存失敗,錯誤 {{ .err }}, 請重試!" -ErrLicenseSync: "許可證信息同步失敗,資料庫中未檢測到許可證信息!" -ErrXpackNotFound: "該部分為專業版功能,請先在 面板設置-許可證 界面導入許可證" -ErrXpackNotActive: "該部分為專業版功能,請先在 面板設置-許可證 界面同步許可證狀態" -ErrXpackOutOfDate: "當前許可證已過期,請重新在 面板設置-許可證 界面導入許可證" - -#task -TaskStart: "{{.name}} 開始 [START]" -TaskEnd: "{{.name}} 結束 [COMPLETED]" -TaskFailed: "{{.name}} 失敗: {{.err}}" -TaskTimeout: "{{.name}} 逾時" -TaskSuccess: "{{.name}} 成功" -TaskRetry: "開始第 {{.name}} 次重試" -SubTaskStart: "開始 {{.name}}" diff --git a/backend/i18n/lang/zh.yaml b/backend/i18n/lang/zh.yaml deleted file mode 100644 index ff3bb1346..000000000 --- a/backend/i18n/lang/zh.yaml +++ /dev/null @@ -1,210 +0,0 @@ -ErrInvalidParams: "请求参数错误: {{ .detail }}" -ErrTokenParse: "Token 生成错误: {{ .detail }}" -ErrInitialPassword: "原密码错误" -ErrInternalServer: "服务内部错误: {{ .detail }}" -ErrRecordExist: "记录已存在" -ErrRecordNotFound: "记录未能找到" -ErrStructTransform: "类型转换失败: {{ .detail }}" -ErrNotLogin: "用户未登录: {{ .detail }}" -ErrPasswordExpired: "当前密码已过期: {{ .detail }}" -ErrNotSupportType: "系统暂不支持当前类型: {{ .detail }}" - -#common -ErrNameIsExist: "名称已存在" -ErrDemoEnvironment: "演示服务器,禁止此操作!" -ErrCmdTimeout: "命令执行超时!" -ErrCmdIllegal: "执行命令中存在不合法字符,请修改后重试!" -ErrPortExist: '{{ .port }} 端口已被 {{ .type }} [{{ .name }}] 占用' -TYPE_APP: "应用" -TYPE_RUNTIME: "运行环境" -TYPE_DOMAIN: "域名" -ErrTypePort: '端口 {{ .name }} 格式错误' -ErrTypePortRange: '端口范围需要在 1-65535 之间' -Success: "成功" -Failed: "失败" -SystemRestart: "系统重启导致任务中断" - -#app -ErrPortInUsed: "{{ .detail }} 端口已被占用!" -ErrAppLimit: "应用超出安装数量限制" -ErrAppRequired: "请先安装 {{ .detail }} 应用" -ErrNotInstall: "应用未安装" -ErrPortInOtherApp: "{{ .port }} 端口已被应用 {{ .apps }} 占用!" -ErrDbUserNotValid: "存量数据库,用户名密码不匹配!" -ErrDockerComposeNotValid: "docker-compose 文件格式错误" -ErrUpdateBuWebsite: '应用更新成功,但是网站配置文件修改失败,请检查配置!' -Err1PanelNetworkFailed: '默认容器网络创建失败!{{ .detail }}' -ErrFileParse: '应用 docker-compose 文件解析失败!' -ErrInstallDirNotFound: '安装目录不存在' -AppStoreIsUpToDate: '应用商店已经是最新版本' -LocalAppVersionNull: '{{.name}} 应用未同步到版本!无法添加到应用列表' -LocalAppVersionErr: '{{.name}} 同步版本 {{.version}} 失败!{{.err}}' -ErrFileNotFound: '{{.name}} 文件不存在' -ErrFileParseApp: '{{.name}} 文件解析失败 {{.err}}' -ErrAppDirNull: '版本文件夹不存在' -LocalAppErr: "应用 {{.name}} 同步失败!{{.err}}" -ErrContainerName: "容器名称已存在" -ErrAppSystemRestart: "1Panel 重启导致任务终止" -ErrCreateHttpClient: "创建HTTP请求失败 {{.err}}" -ErrHttpReqTimeOut: "请求超时 {{.err}}" -ErrHttpReqFailed: "请求失败 {{.err}}" -ErrHttpReqNotFound: "文件不存在" -ErrNoSuchHost: "网络连接失败" -ErrImagePullTimeOut: '镜像拉取超时' -ErrContainerNotFound: '{{ .name }} 容器不存在' -ErrContainerMsg: '{{ .name }} 容器异常,具体请在容器页面查看日志' -ErrAppBackup: '{{ .name }} 应用备份失败 err {{.err}}' -ErrImagePull: '镜像拉取失败 {{.err}}' -ErrVersionTooLow: '当前 1Panel 版本过低,无法更新应用商店,请升级版本之后操作' -ErrAppNameExist: '应用名称已存在' -AppStoreIsSyncing: '应用商店正在同步中,请稍后再试' -ErrGetCompose: "docker-compose.yml 文件获取失败!{{ .detail }}" -ErrAppWarn: "状态异常,请查看日志" -ErrAppParamKey: "参数 {{ .name }} 字段异常" -ErrAppUpgrade: "应用 {{ .name }} 升级失败 {{ .err }}" -AppRecover: "应用 {{ .name }} 回滚 " -PullImageStart: "开始拉取镜像 {{ .name }}" -PullImageSuccess: "镜像拉取成功" -UpgradeAppStart: "开始升级应用 {{ .name }}" -UpgradeAppSuccess: "应用 {{ .name }} 升级成功" - -#file -ErrFileCanNotRead: "此文件不支持预览" -ErrFileToLarge: "文件超过10M,无法打开" -ErrPathNotFound: "目录不存在" -ErrMovePathFailed: "目标路径不能包含原路径!" -ErrLinkPathNotFound: "目标路径不存在!" -ErrFileIsExist: "文件或文件夹已存在!" -ErrFileUpload: "{{ .name }} 上传文件失败 {{ .detail}}" -ErrFileDownloadDir: "不支持下载文件夹" -ErrCmdNotFound: "{{ .name}} 命令不存在,请先在宿主机安装此命令" -ErrSourcePathNotFound: "源目录不存在" -ErrFavoriteExist: "已收藏此路径" -ErrInvalidChar: "禁止使用非法字符" - -#website -ErrDomainIsExist: "域名已存在" -ErrAliasIsExist: "代号已存在" -ErrAppDelete: '其他网站使用此应用,无法删除' -ErrGroupIsUsed: '分组正在使用中,无法删除' -ErrBackupMatch: '该备份文件与当前网站部分数据不匹配 {{ .detail}}' -ErrBackupExist: '该备份文件对应部分源数据不存在 {{ .detail}}' -ErrPHPResource: '本地运行环境不支持切换!' -ErrPathPermission: 'index 目录下检测到非 1000:1000 权限文件夹,可能导致网站访问 Access denied 错误,请点击上方保存按钮' -ErrDomainIsUsed: "域名已被网站【{{ .name }}】使用" -ErrDomainFormat: "{{ .name }} 域名格式不正确" -ErrDefaultAlias: "default 为保留代号,请使用其他代号" - -#ssl -ErrSSLCannotDelete: "{{ .name }} 证书正在被网站使用,无法删除" -ErrAccountCannotDelete: "账号关联证书,无法删除" -ErrSSLApply: "证书续签成功,openresty reload失败,请检查配置!" -ErrEmailIsExist: '邮箱已存在' -ErrSSLKeyNotFound: '私钥文件不存在' -ErrSSLCertificateNotFound: '证书文件不存在' -ErrSSLKeyFormat: '私钥文件校验失败' -ErrSSLCertificateFormat: '证书文件格式错误,请使用 pem 格式' -ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid 或 EabHmacKey 不能为空' -ErrOpenrestyNotFound: 'Http 模式需要首先安装 Openresty' -ApplySSLStart: '开始申请证书,域名 [{{ .domain }}] 申请方式 [{{ .type }}] ' -dnsAccount: "DNS 自动" -dnsManual: "DNS 手动" -http: "HTTP" -ApplySSLFailed: '申请 [{{ .domain }}] 证书失败, {{.detail}} ' -ApplySSLSuccess: '申请 [{{ .domain }}] 证书成功!!' -DNSAccountName: 'DNS 账号 [{{ .name }}] 厂商 [{{.type}}]' -PushDirLog: '证书推送到目录 [{{ .path }}] {{ .status }}' -ErrDeleteCAWithSSL: "当前机构下存在已签发证书,无法删除" -ErrDeleteWithPanelSSL: "面板 SSL 配置使用此证书,无法删除" -ErrDefaultCA: "默认机构不能删除" -ApplyWebSiteSSLLog: "开始更新 {{ .name }} 网站证书" -ErrUpdateWebsiteSSL: "{{ .name }} 网站更新证书失败: {{ .err }}" -ApplyWebSiteSSLSuccess: "更新网站证书成功" -ErrExecShell: "执行脚本失败 {{ .err }}" -ExecShellStart: "开始执行脚本" -ExecShellSuccess: "脚本执行成功" - -#mysql -ErrUserIsExist: "当前用户已存在,请重新输入" -ErrDatabaseIsExist: "当前数据库已存在,请重新输入" -ErrExecTimeOut: "SQL 执行超时,请检查数据库" -ErrRemoteExist: "远程数据库已存在该名称,请修改后重试" -ErrLocalExist: "本地数据库已存在该名称,请修改后重试" - -#redis -ErrTypeOfRedis: "恢复文件类型与当前持久化方式不符,请修改后重试" - -#container -ErrInUsed: "{{ .detail }} 正被使用,无法删除" -ErrObjectInUsed: "该对象正被使用,无法删除" -ErrPortRules: "端口数目不匹配,请重新输入!" -ErrPgImagePull: "镜像拉取超时,请配置镜像加速或手动拉取 postgres:16.0-alpine 镜像后重试" - -#runtime -ErrDirNotFound: "build 文件夹不存在!请检查文件完整性!" -ErrFileNotExist: "{{ .detail }} 文件不存在!请检查源文件完整性!" -ErrImageBuildErr: "镜像 build 失败" -ErrImageExist: "镜像已存在!" -ErrDelWithWebsite: "运行环境已经关联网站,无法删除" -ErrRuntimeStart: "启动失败" -ErrPackageJsonNotFound: "package.json 文件不存在" -ErrScriptsNotFound: "没有在 package.json 中找到 scripts 配置项" -ErrContainerNameNotFound: "无法获取容器名称,请检查 .env 文件" -ErrNodeModulesNotFound: "node_modules 文件夹不存在!请编辑运行环境或者等待运行环境启动成功" - -#setting -ErrBackupInUsed: "该备份账号已在计划任务中使用,无法删除" -ErrBackupCheck: "备份账号测试连接失败 {{ .err}}" -ErrOSSConn: "无法获取最新版本,请确认服务器是否能够连接外部网络。" -ErrEntrance: "安全入口信息错误,请检查后重试!" - -#tool -ErrConfigNotFound: "配置文件不存在" -ErrConfigParse: "配置文件格式有误" -ErrConfigIsNull: "配置文件不允许为空" -ErrConfigDirNotFound: "运行目录不存在" -ErrConfigAlreadyExist: "已存在同名配置文件" -ErrUserFindErr: "用户 {{ .name }} 查找失败 {{ .err }}" - -#ssh -ErrFirewall: "当前未检测到系统 firewalld 或 ufw 服务,请检查后重试!" - -#cronjob -ErrBashExecute: "脚本执行错误,请在任务输出文本域中查看具体信息。" -ErrCutWebsiteLog: "{{ .name }} 网站日志切割失败,错误 {{ .err }}" -CutWebsiteLogSuccess: "{{ .name }} 网站日志切割成功,备份路径 {{ .path }}" - -#toolbox -ErrNotExistUser: "当前用户不存在,请修改后重试!" -ErrBanAction: "设置失败,当前 {{ .name }} 服务不可用,请检查后重试!" -ErrClamdscanNotFound: "未检测到 clamdscan 命令,请参考文档安装!" - -#waf -ErrScope: "不支持修改此配置" -ErrStateChange: "状态修改失败" -ErrRuleExist: "规则已存在" -ErrRuleNotExist: "规则不存在" -ErrParseIP: "IP 格式错误" -ErrDefaultIP: "default 为保留名称,请更换其他名称" -ErrGroupInUse: "IP 组被黑/白名单使用,无法删除" -ErrGroupExist: "IP 组名称已存在" -ErrIPRange: "IP 范围错误" -ErrIPExist: "IP 已存在" - -#license -ErrLicense: "许可证格式错误,请检查后重试!" -ErrLicenseCheck: "许可证校验失败,请检查后重试!" -ErrLicenseSave: "许可证信息保存失败,错误 {{ .err }},请重试!" -ErrLicenseSync: "许可证信息同步失败,数据库中未检测到许可证信息!" -ErrXpackNotFound: "该部分为专业版功能,请先在 面板设置-许可证 界面导入许可证" -ErrXpackNotActive: "该部分为专业版功能,请先在 面板设置-许可证 界面同步许可证状态" -ErrXpackOutOfDate: "当前许可证已过期,请重新在 面板设置-许可证 界面导入许可证" - -#task -TaskStart: "{{.name}} 开始 [START]" -TaskEnd: "{{.name}} 结束 [COMPLETED]" -TaskFailed: "{{.name}} 失败: {{.err}}" -TaskTimeout: "{{.name}} 超时" -TaskSuccess: "{{.name}} 成功" -TaskRetry: "开始第 {{.name}} 次重试" -SubTaskStart: "开始 {{.name}}" diff --git a/backend/init/app/app.go b/backend/init/app/app.go deleted file mode 100644 index 51cc8c46d..000000000 --- a/backend/init/app/app.go +++ /dev/null @@ -1,46 +0,0 @@ -package app - -import ( - "github.com/1Panel-dev/1Panel/backend/utils/docker" - "github.com/1Panel-dev/1Panel/backend/utils/firewall" - "path" - - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/files" -) - -func Init() { - constant.DataDir = global.CONF.System.DataDir - constant.ResourceDir = path.Join(constant.DataDir, "resource") - constant.AppResourceDir = path.Join(constant.ResourceDir, "apps") - constant.AppInstallDir = path.Join(constant.DataDir, "apps") - constant.RuntimeDir = path.Join(constant.DataDir, "runtime") - - constant.LocalAppResourceDir = path.Join(constant.AppResourceDir, "local") - constant.LocalAppInstallDir = path.Join(constant.AppInstallDir, "local") - constant.RemoteAppResourceDir = path.Join(constant.AppResourceDir, "remote") - - constant.LogDir = path.Join(global.CONF.System.DataDir, "log") - constant.SSLLogDir = path.Join(constant.LogDir, "ssl") - - dirs := []string{constant.DataDir, constant.ResourceDir, constant.AppResourceDir, constant.AppInstallDir, - global.CONF.System.Backup, constant.RuntimeDir, constant.LocalAppResourceDir, constant.RemoteAppResourceDir, constant.SSLLogDir} - - fileOp := files.NewFileOp() - for _, dir := range dirs { - createDir(fileOp, dir) - } - - _ = docker.CreateDefaultDockerNetwork() - - if f, err := firewall.NewFirewallClient(); err == nil { - _ = f.EnableForward() - } -} - -func createDir(fileOp files.FileOp, dirPath string) { - if !fileOp.Stat(dirPath) { - _ = fileOp.CreateDir(dirPath, 0755) - } -} diff --git a/backend/init/business/business.go b/backend/init/business/business.go deleted file mode 100644 index 4a544b5da..000000000 --- a/backend/init/business/business.go +++ /dev/null @@ -1,40 +0,0 @@ -package business - -import ( - "github.com/1Panel-dev/1Panel/backend/app/service" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" -) - -func Init() { - go syncApp() - go syncInstalledApp() - go syncRuntime() - go syncSSL() -} - -func syncApp() { - _ = service.NewISettingService().Update("AppStoreSyncStatus", constant.SyncSuccess) - if err := service.NewIAppService().SyncAppListFromRemote(); err != nil { - global.LOG.Errorf("App Store synchronization failed") - return - } -} - -func syncInstalledApp() { - if err := service.NewIAppInstalledService().SyncAll(true); err != nil { - global.LOG.Errorf("sync installed app error: %s", err.Error()) - } -} - -func syncRuntime() { - if err := service.NewRuntimeService().SyncForRestart(); err != nil { - global.LOG.Errorf("sync runtime status error : %s", err.Error()) - } -} - -func syncSSL() { - if err := service.NewIWebsiteSSLService().SyncForRestart(); err != nil { - global.LOG.Errorf("sync ssl status error : %s", err.Error()) - } -} diff --git a/backend/init/cache/badger_db/badger_db.go b/backend/init/cache/badger_db/badger_db.go deleted file mode 100644 index a1508503a..000000000 --- a/backend/init/cache/badger_db/badger_db.go +++ /dev/null @@ -1,79 +0,0 @@ -package badger_db - -import ( - "fmt" - "time" - - "github.com/dgraph-io/badger/v4" -) - -type Cache struct { - db *badger.DB -} - -func NewCacheDB(db *badger.DB) *Cache { - return &Cache{ - db: db, - } -} - -func (c *Cache) Set(key string, value interface{}) error { - err := c.db.Update(func(txn *badger.Txn) error { - v := []byte(fmt.Sprintf("%v", value)) - return txn.Set([]byte(key), v) - }) - return err -} - -func (c *Cache) Del(key string) error { - err := c.db.Update(func(txn *badger.Txn) error { - return txn.Delete([]byte(key)) - }) - return err -} - -func (c *Cache) Clean() error { - return c.db.DropAll() -} - -func (c *Cache) Get(key string) ([]byte, error) { - var result []byte - err := c.db.View(func(txn *badger.Txn) error { - item, err := txn.Get([]byte(key)) - if err != nil { - return err - } - err = item.Value(func(val []byte) error { - result = append([]byte{}, val...) - return nil - }) - return err - }) - return result, err -} - -func (c *Cache) SetWithTTL(key string, value interface{}, duration time.Duration) error { - err := c.db.Update(func(txn *badger.Txn) error { - v := []byte(fmt.Sprintf("%v", value)) - e := badger.NewEntry([]byte(key), v).WithTTL(duration) - return txn.SetEntry(e) - }) - return err -} - -func (c *Cache) PrefixScanKey(prefixStr string) ([]string, error) { - var res []string - err := c.db.View(func(txn *badger.Txn) error { - it := txn.NewIterator(badger.DefaultIteratorOptions) - defer it.Close() - prefix := []byte(prefixStr) - for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { - item := it.Item() - k := item.Key() - res = append(res, string(k)) - return nil - } - return nil - }) - return res, err -} diff --git a/backend/init/cache/cache.go b/backend/init/cache/cache.go deleted file mode 100644 index 1b64e1f72..000000000 --- a/backend/init/cache/cache.go +++ /dev/null @@ -1,56 +0,0 @@ -package cache - -import ( - "time" - - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/init/cache/badger_db" - "github.com/dgraph-io/badger/v4" -) - -func Init() { - c := global.CONF.System.Cache - - options := badger.Options{ - Dir: c, - ValueDir: c, - ValueLogFileSize: 64 << 20, - ValueLogMaxEntries: 10 << 20, - VLogPercentile: 0.1, - - MemTableSize: 32 << 20, - BaseTableSize: 2 << 20, - BaseLevelSize: 10 << 20, - TableSizeMultiplier: 2, - LevelSizeMultiplier: 10, - MaxLevels: 7, - NumGoroutines: 4, - MetricsEnabled: true, - NumCompactors: 2, - NumLevelZeroTables: 5, - NumLevelZeroTablesStall: 15, - NumMemtables: 1, - BloomFalsePositive: 0.01, - BlockSize: 2 * 1024, - SyncWrites: false, - NumVersionsToKeep: 1, - CompactL0OnClose: false, - VerifyValueChecksum: false, - BlockCacheSize: 32 << 20, - IndexCacheSize: 0, - ZSTDCompressionLevel: 1, - EncryptionKey: []byte{}, - EncryptionKeyRotationDuration: 10 * 24 * time.Hour, // Default 10 days. - DetectConflicts: true, - NamespaceOffset: -1, - } - - cache, err := badger.Open(options) - if err != nil { - panic(err) - } - _ = cache.DropAll() - global.CacheDb = cache - global.CACHE = badger_db.NewCacheDB(cache) - global.LOG.Info("init cache successfully") -} diff --git a/backend/init/db/db.go b/backend/init/db/db.go deleted file mode 100644 index e0cd1bc8b..000000000 --- a/backend/init/db/db.go +++ /dev/null @@ -1,94 +0,0 @@ -package db - -import ( - "fmt" - "log" - "os" - "path" - "time" - - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/glebarez/sqlite" - "gorm.io/gorm" - "gorm.io/gorm/logger" -) - -func Init() { - if _, err := os.Stat(global.CONF.System.DbPath); err != nil { - if err := os.MkdirAll(global.CONF.System.DbPath, os.ModePerm); err != nil { - panic(fmt.Errorf("init db dir failed, err: %v", err)) - } - } - fullPath := global.CONF.System.DbPath + "/" + global.CONF.System.DbFile - if _, err := os.Stat(fullPath); err != nil { - f, err := os.Create(fullPath) - if err != nil { - panic(fmt.Errorf("init db file failed, err: %v", err)) - } - _ = f.Close() - } - - newLogger := logger.New( - log.New(os.Stdout, "\r\n", log.LstdFlags), - logger.Config{ - SlowThreshold: time.Second, - LogLevel: logger.Silent, - IgnoreRecordNotFoundError: true, - Colorful: false, - }, - ) - initMonitorDB(newLogger) - - db, err := gorm.Open(sqlite.Open(fullPath), &gorm.Config{ - DisableForeignKeyConstraintWhenMigrating: true, - Logger: newLogger, - }) - if err != nil { - panic(err) - } - _ = db.Exec("PRAGMA journal_mode = WAL;") - sqlDB, dbError := db.DB() - if dbError != nil { - panic(dbError) - } - sqlDB.SetConnMaxIdleTime(10) - sqlDB.SetMaxOpenConns(100) - sqlDB.SetConnMaxLifetime(time.Hour) - - global.DB = db - global.LOG.Info("init db successfully") -} - -func initMonitorDB(newLogger logger.Interface) { - if _, err := os.Stat(global.CONF.System.DbPath); err != nil { - if err := os.MkdirAll(global.CONF.System.DbPath, os.ModePerm); err != nil { - panic(fmt.Errorf("init db dir failed, err: %v", err)) - } - } - fullPath := path.Join(global.CONF.System.DbPath, "monitor.db") - if _, err := os.Stat(fullPath); err != nil { - f, err := os.Create(fullPath) - if err != nil { - panic(fmt.Errorf("init db file failed, err: %v", err)) - } - _ = f.Close() - } - - db, err := gorm.Open(sqlite.Open(fullPath), &gorm.Config{ - DisableForeignKeyConstraintWhenMigrating: true, - Logger: newLogger, - }) - if err != nil { - panic(err) - } - sqlDB, dbError := db.DB() - if dbError != nil { - panic(dbError) - } - sqlDB.SetConnMaxIdleTime(10) - sqlDB.SetMaxOpenConns(100) - sqlDB.SetConnMaxLifetime(time.Hour) - - global.MonitorDB = db - global.LOG.Info("init monitor db successfully") -} diff --git a/backend/init/hook/hook.go b/backend/init/hook/hook.go deleted file mode 100644 index 8f02d4a79..000000000 --- a/backend/init/hook/hook.go +++ /dev/null @@ -1,147 +0,0 @@ -package hook - -import ( - "encoding/base64" - "encoding/json" - "os" - "path" - - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/app/repo" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" -) - -func Init() { - settingRepo := repo.NewISettingRepo() - OneDriveID, err := settingRepo.Get(settingRepo.WithByKey("OneDriveID")) - if err != nil { - global.LOG.Errorf("load onedrive info from setting failed, err: %v", err) - } - idItem, _ := base64.StdEncoding.DecodeString(OneDriveID.Value) - global.CONF.System.OneDriveID = string(idItem) - OneDriveSc, err := settingRepo.Get(settingRepo.WithByKey("OneDriveSc")) - if err != nil { - global.LOG.Errorf("load onedrive info from setting failed, err: %v", err) - } - scItem, _ := base64.StdEncoding.DecodeString(OneDriveSc.Value) - global.CONF.System.OneDriveSc = string(scItem) - - if _, err := settingRepo.Get(settingRepo.WithByKey("SystemStatus")); err != nil { - _ = settingRepo.Create("SystemStatus", "Free") - } - if err := settingRepo.Update("SystemStatus", "Free"); err != nil { - global.LOG.Fatalf("init service before start failed, err: %v", err) - } - - handleCronjobStatus() - handleSnapStatus() - loadLocalDir() - initDir() -} - -func handleSnapStatus() { - msgFailed := "the task was interrupted due to the restart of the 1panel service" - _ = global.DB.Model(&model.Snapshot{}).Where("status = ?", "OnSaveData"). - Updates(map[string]interface{}{"status": constant.StatusSuccess}).Error - - _ = global.DB.Model(&model.Snapshot{}).Where("status = ?", constant.StatusWaiting). - Updates(map[string]interface{}{ - "status": constant.StatusFailed, - "message": msgFailed, - }).Error - - _ = global.DB.Model(&model.Snapshot{}).Where("recover_status = ?", constant.StatusWaiting). - Updates(map[string]interface{}{ - "recover_status": constant.StatusFailed, - "recover_message": msgFailed, - }).Error - - _ = global.DB.Model(&model.Snapshot{}).Where("rollback_status = ?", constant.StatusWaiting). - Updates(map[string]interface{}{ - "rollback_status": constant.StatusFailed, - "rollback_message": msgFailed, - }).Error - - snapRepo := repo.NewISnapshotRepo() - - status, _ := snapRepo.GetStatusList() - for _, item := range status { - updates := make(map[string]interface{}) - if item.Panel == constant.StatusRunning { - updates["panel"] = constant.StatusFailed - } - if item.PanelInfo == constant.StatusRunning { - updates["panel_info"] = constant.StatusFailed - } - if item.DaemonJson == constant.StatusRunning { - updates["daemon_json"] = constant.StatusFailed - } - if item.AppData == constant.StatusRunning { - updates["app_data"] = constant.StatusFailed - } - if item.PanelData == constant.StatusRunning { - updates["panel_data"] = constant.StatusFailed - } - if item.BackupData == constant.StatusRunning { - updates["backup_data"] = constant.StatusFailed - } - if item.Compress == constant.StatusRunning { - updates["compress"] = constant.StatusFailed - } - if item.Upload == constant.StatusUploading { - updates["upload"] = constant.StatusFailed - } - if len(updates) != 0 { - _ = snapRepo.UpdateStatus(item.ID, updates) - } - } -} - -func handleCronjobStatus() { - _ = global.DB.Model(&model.JobRecords{}).Where("status = ?", constant.StatusWaiting). - Updates(map[string]interface{}{ - "status": constant.StatusFailed, - "message": "the task was interrupted due to the restart of the 1panel service", - }).Error -} - -func loadLocalDir() { - var backup model.BackupAccount - _ = global.DB.Where("type = ?", "LOCAL").First(&backup).Error - if backup.ID == 0 { - global.LOG.Errorf("no such backup account `%s` in db", "LOCAL") - return - } - varMap := make(map[string]interface{}) - if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil { - global.LOG.Errorf("json unmarshal backup.Vars: %v failed, err: %v", backup.Vars, err) - return - } - if _, ok := varMap["dir"]; !ok { - global.LOG.Error("load local backup dir failed") - return - } - baseDir, ok := varMap["dir"].(string) - if ok { - if _, err := os.Stat(baseDir); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(baseDir, os.ModePerm); err != nil { - global.LOG.Errorf("mkdir %s failed, err: %v", baseDir, err) - return - } - } - global.CONF.System.Backup = baseDir - return - } - global.LOG.Errorf("error type dir: %T", varMap["dir"]) -} - -func initDir() { - composePath := path.Join(global.CONF.System.BaseDir, "1panel/docker/compose/") - if _, err := os.Stat(composePath); err != nil && os.IsNotExist(err) { - if err = os.MkdirAll(composePath, os.ModePerm); err != nil { - global.LOG.Errorf("mkdir %s failed, err: %v", composePath, err) - return - } - } -} diff --git a/backend/init/log/log.go b/backend/init/log/log.go deleted file mode 100644 index 9e1128e2d..000000000 --- a/backend/init/log/log.go +++ /dev/null @@ -1,68 +0,0 @@ -package log - -import ( - "fmt" - "io" - "os" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/log" - - "github.com/1Panel-dev/1Panel/backend/configs" - "github.com/1Panel-dev/1Panel/backend/global" - - "github.com/sirupsen/logrus" -) - -const ( - TimeFormat = "2006-01-02 15:04:05" - FileTImeFormat = "2006-01-02" - RollingTimePattern = "0 0 * * *" -) - -func Init() { - l := logrus.New() - setOutput(l, global.CONF.LogConfig) - global.LOG = l - global.LOG.Info("init logger successfully") -} - -func setOutput(logger *logrus.Logger, config configs.LogConfig) { - writer, err := log.NewWriterFromConfig(&log.Config{ - LogPath: global.CONF.System.LogPath, - FileName: config.LogName, - TimeTagFormat: FileTImeFormat, - MaxRemain: config.MaxBackup, - RollingTimePattern: RollingTimePattern, - LogSuffix: config.LogSuffix, - }) - if err != nil { - panic(err) - } - level, err := logrus.ParseLevel(config.Level) - if err != nil { - panic(err) - } - fileAndStdoutWriter := io.MultiWriter(writer, os.Stdout) - - logger.SetOutput(fileAndStdoutWriter) - logger.SetLevel(level) - logger.SetFormatter(new(MineFormatter)) -} - -type MineFormatter struct{} - -func (s *MineFormatter) Format(entry *logrus.Entry) ([]byte, error) { - detailInfo := "" - if entry.Caller != nil { - function := strings.ReplaceAll(entry.Caller.Function, "github.com/1Panel-dev/1Panel/backend/", "") - detailInfo = fmt.Sprintf("(%s: %d)", function, entry.Caller.Line) - } - if len(entry.Data) == 0 { - msg := fmt.Sprintf("[%s] [%s] %s %s \n", time.Now().Format(TimeFormat), strings.ToUpper(entry.Level.String()), entry.Message, detailInfo) - return []byte(msg), nil - } - msg := fmt.Sprintf("[%s] [%s] %s %s {%v} \n", time.Now().Format(TimeFormat), strings.ToUpper(entry.Level.String()), entry.Message, detailInfo, entry.Data) - return []byte(msg), nil -} diff --git a/backend/init/migration/migrate.go b/backend/init/migration/migrate.go deleted file mode 100644 index 773f0f299..000000000 --- a/backend/init/migration/migrate.go +++ /dev/null @@ -1,26 +0,0 @@ -package migration - -import ( - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/init/migration/migrations" - - "github.com/go-gormigrate/gormigrate/v2" -) - -func Init() { - m := gormigrate.New(global.DB, gormigrate.DefaultOptions, []*gormigrate.Migration{ - migrations.AddTable, - migrations.InitHost, - migrations.InitSetting, - migrations.InitBackupAccount, - migrations.InitImageRepo, - migrations.InitDefaultGroup, - migrations.InitDefaultCA, - migrations.InitPHPExtensions, - }) - if err := m.Migrate(); err != nil { - global.LOG.Error(err) - panic(err) - } - global.LOG.Info("Migration run successfully") -} diff --git a/backend/init/migration/migrations/init.go b/backend/init/migration/migrations/init.go deleted file mode 100644 index 9a3d96822..000000000 --- a/backend/init/migration/migrations/init.go +++ /dev/null @@ -1,249 +0,0 @@ -package migrations - -import ( - "fmt" - - "github.com/1Panel-dev/1Panel/backend/app/dto/request" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/app/service" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/common" - - "github.com/go-gormigrate/gormigrate/v2" - "gorm.io/gorm" -) - -var AddTable = &gormigrate.Migration{ - ID: "20240722-add-table", - Migrate: func(tx *gorm.DB) error { - return tx.AutoMigrate( - &model.AppDetail{}, - &model.AppInstallResource{}, - &model.AppInstall{}, - &model.AppTag{}, - &model.Tag{}, - &model.App{}, - &model.BackupAccount{}, - &model.BackupRecord{}, - &model.Clam{}, - &model.Command{}, - &model.ComposeTemplate{}, - &model.Compose{}, - &model.Cronjob{}, - &model.Database{}, - &model.DatabaseMysql{}, - &model.DatabasePostgresql{}, - &model.Favorite{}, - &model.Forward{}, - &model.Firewall{}, - &model.Ftp{}, - &model.Group{}, - &model.Host{}, - &model.ImageRepo{}, - &model.JobRecords{}, - &model.MonitorBase{}, - &model.MonitorIO{}, - &model.MonitorNetwork{}, - &model.PHPExtensions{}, - &model.RedisCommand{}, - &model.Runtime{}, - &model.Setting{}, - &model.Snapshot{}, - &model.SnapshotStatus{}, - &model.Tag{}, - &model.Website{}, - &model.WebsiteAcmeAccount{}, - &model.WebsiteCA{}, - &model.WebsiteDnsAccount{}, - &model.WebsiteDomain{}, - &model.WebsiteSSL{}, - ) - }, -} - -var InitHost = &gormigrate.Migration{ - ID: "20240722-init-host", - Migrate: func(tx *gorm.DB) error { - group := model.Group{ - Name: "default", Type: "host", IsDefault: true, - } - if err := tx.Create(&group).Error; err != nil { - return err - } - host := model.Host{ - Name: "localhost", Addr: "127.0.0.1", User: "root", Port: 22, AuthMode: "password", GroupID: group.ID, - } - if err := tx.Create(&host).Error; err != nil { - return err - } - return nil - }, -} - -var InitSetting = &gormigrate.Migration{ - ID: "20240722-init-setting", - Migrate: func(tx *gorm.DB) error { - if err := tx.Create(&model.Setting{Key: "SystemIP", Value: ""}).Error; err != nil { - return err - } - if err := tx.Create(&model.Setting{Key: "DockerSockPath", Value: "unix:///var/run/docker.sock"}).Error; err != nil { - return err - } - if err := tx.Create(&model.Setting{Key: "SystemVersion", Value: global.CONF.System.Version}).Error; err != nil { - return err - } - if err := tx.Create(&model.Setting{Key: "SystemStatus", Value: "Free"}).Error; err != nil { - return err - } - - if err := tx.Create(&model.Setting{Key: "LocalTime", Value: ""}).Error; err != nil { - return err - } - if err := tx.Create(&model.Setting{Key: "TimeZone", Value: common.LoadTimeZoneByCmd()}).Error; err != nil { - return err - } - if err := tx.Create(&model.Setting{Key: "NtpSite", Value: "pool.ntp.org"}).Error; err != nil { - return err - } - - if err := tx.Create(&model.Setting{Key: "DefaultNetwork", Value: "all"}).Error; err != nil { - return err - } - if err := tx.Create(&model.Setting{Key: "LastCleanTime", Value: ""}).Error; err != nil { - return err - } - if err := tx.Create(&model.Setting{Key: "LastCleanSize", Value: ""}).Error; err != nil { - return err - } - if err := tx.Create(&model.Setting{Key: "LastCleanData", Value: ""}).Error; err != nil { - return err - } - - if err := tx.Create(&model.Setting{Key: "MonitorStatus", Value: "enable"}).Error; err != nil { - return err - } - if err := tx.Create(&model.Setting{Key: "MonitorStoreDays", Value: "7"}).Error; err != nil { - return err - } - if err := tx.Create(&model.Setting{Key: "MonitorInterval", Value: "5"}).Error; err != nil { - return err - } - - if err := tx.Create(&model.Setting{Key: "AppStoreVersion", Value: ""}).Error; err != nil { - return err - } - if err := tx.Create(&model.Setting{Key: "AppStoreSyncStatus", Value: "SyncSuccess"}).Error; err != nil { - return err - } - if err := tx.Create(&model.Setting{Key: "AppStoreLastModified", Value: "0"}).Error; err != nil { - return err - } - - if err := tx.Create(&model.Setting{Key: "OneDriveID", Value: "MDEwOTM1YTktMWFhOS00ODU0LWExZGMtNmU0NWZlNjI4YzZi"}).Error; err != nil { - return err - } - if err := tx.Create(&model.Setting{Key: "OneDriveSc", Value: "akpuOFF+YkNXOU1OLWRzS1ZSRDdOcG1LT2ZRM0RLNmdvS1RkVWNGRA=="}).Error; err != nil { - return err - } - - if err := tx.Create(&model.Setting{Key: "FileRecycleBin", Value: "enable"}).Error; err != nil { - return err - } - if err := tx.Create(&model.Setting{Key: "SnapshotIgnore", Value: "*.sock"}).Error; err != nil { - return err - } - return nil - }, -} - -var InitBackupAccount = &gormigrate.Migration{ - ID: "20240722-init-backup", - Migrate: func(tx *gorm.DB) error { - item := &model.BackupAccount{ - Type: "LOCAL", - Vars: fmt.Sprintf("{\"dir\":\"%s\"}", global.CONF.System.Backup), - } - if err := tx.Create(item).Error; err != nil { - return err - } - return nil - }, -} - -var InitImageRepo = &gormigrate.Migration{ - ID: "20240722-init-imagerepo", - Migrate: func(tx *gorm.DB) error { - item := &model.ImageRepo{ - Name: "Docker Hub", - Protocol: "https", - DownloadUrl: "docker.io", - Status: constant.StatusSuccess, - } - if err := tx.Create(item).Error; err != nil { - return err - } - return nil - }, -} - -var InitDefaultGroup = &gormigrate.Migration{ - ID: "20240722-init-default-group", - Migrate: func(tx *gorm.DB) error { - websiteGroup := &model.Group{ - Name: "默认", - IsDefault: true, - Type: "website", - } - if err := tx.Create(websiteGroup).Error; err != nil { - return err - } - commandGroup := &model.Group{IsDefault: true, Name: "默认", Type: "command"} - if err := tx.Create(commandGroup).Error; err != nil { - return err - } - return nil - }, -} - -var InitDefaultCA = &gormigrate.Migration{ - ID: "20240722-init-default-ca", - Migrate: func(tx *gorm.DB) error { - caService := service.NewIWebsiteCAService() - if _, err := caService.Create(request.WebsiteCACreate{ - CommonName: "1Panel-CA", - Country: "CN", - KeyType: "P256", - Name: "1Panel", - Organization: "FIT2CLOUD", - OrganizationUint: "1Panel", - Province: "Beijing", - City: "Beijing", - }); err != nil { - return err - } - return nil - }, -} - -var InitPHPExtensions = &gormigrate.Migration{ - ID: "20240722-add-php-extensions", - Migrate: func(tx *gorm.DB) error { - if err := tx.Create(&model.PHPExtensions{Name: "默认", Extensions: "bcmath,gd,gettext,intl,pcntl,shmop,soap,sockets,sysvsem,xmlrpc,zip"}).Error; err != nil { - return err - } - if err := tx.Create(&model.PHPExtensions{Name: "WordPress", Extensions: "exif,igbinary,imagick,intl,zip,apcu,memcached,opcache,redis,bc,image,shmop,mysqli,pdo_mysql,gd"}).Error; err != nil { - return err - } - if err := tx.Create(&model.PHPExtensions{Name: "Flarum", Extensions: "curl,gd,pdo_mysql,mysqli,bz2,exif,yaf,imap"}).Error; err != nil { - return err - } - if err := tx.Create(&model.PHPExtensions{Name: "苹果CMS-V10", Extensions: "mysqli,pdo_mysql,zip,gd,redis,memcache,memcached"}).Error; err != nil { - return err - } - if err := tx.Create(&model.PHPExtensions{Name: "SeaCMS", Extensions: "mysqli,pdo_mysql,gd,curl"}).Error; err != nil { - return err - } - return nil - }, -} diff --git a/backend/init/router/router.go b/backend/init/router/router.go deleted file mode 100644 index 217e65be0..000000000 --- a/backend/init/router/router.go +++ /dev/null @@ -1,41 +0,0 @@ -package router - -import ( - "github.com/1Panel-dev/1Panel/backend/i18n" - "github.com/1Panel-dev/1Panel/backend/middleware" - rou "github.com/1Panel-dev/1Panel/backend/router" - "github.com/gin-contrib/gzip" - "github.com/gin-gonic/gin" -) - -var ( - Router *gin.Engine -) - -func setWebStatic(rootRouter *gin.RouterGroup) { - rootRouter.Static("/api/v1/images", "./uploads") - rootRouter.Use(func(c *gin.Context) { - c.Next() - }) -} - -func Routers() *gin.Engine { - Router = gin.Default() - Router.Use(i18n.UseI18n()) - - PublicGroup := Router.Group("") - { - PublicGroup.GET("/health", func(c *gin.Context) { - c.JSON(200, "ok") - }) - PublicGroup.Use(gzip.Gzip(gzip.DefaultCompression)) - setWebStatic(PublicGroup) - } - PrivateGroup := Router.Group("/api/v1") - PrivateGroup.Use(middleware.GlobalLoading()) - for _, router := range rou.RouterGroupApp { - router.InitRouter(PrivateGroup) - } - - return Router -} diff --git a/backend/init/validator/validator.go b/backend/init/validator/validator.go deleted file mode 100644 index d840383bd..000000000 --- a/backend/init/validator/validator.go +++ /dev/null @@ -1,65 +0,0 @@ -package validator - -import ( - "regexp" - "unicode" - - "github.com/1Panel-dev/1Panel/backend/global" - - "github.com/go-playground/validator/v10" -) - -func Init() { - validator := validator.New() - if err := validator.RegisterValidation("name", checkNamePattern); err != nil { - panic(err) - } - if err := validator.RegisterValidation("ip", checkIpPattern); err != nil { - panic(err) - } - if err := validator.RegisterValidation("password", checkPasswordPattern); err != nil { - panic(err) - } - global.VALID = validator -} - -func checkNamePattern(fl validator.FieldLevel) bool { - value := fl.Field().String() - result, err := regexp.MatchString("^[a-zA-Z\u4e00-\u9fa5]{1}[a-zA-Z0-9_\u4e00-\u9fa5]{0,30}$", value) - if err != nil { - global.LOG.Errorf("regexp matchString failed, %v", err) - } - return result -} - -func checkIpPattern(fl validator.FieldLevel) bool { - value := fl.Field().String() - result, err := regexp.MatchString(`^((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}$`, value) - if err != nil { - global.LOG.Errorf("regexp check ip matchString failed, %v", err) - } - return result -} - -func checkPasswordPattern(fl validator.FieldLevel) bool { - value := fl.Field().String() - if len(value) < 8 || len(value) > 30 { - return false - } - - hasNum := false - hasLetter := false - for _, r := range value { - if unicode.IsLetter(r) && !hasLetter { - hasLetter = true - } - if unicode.IsNumber(r) && !hasNum { - hasNum = true - } - if hasLetter && hasNum { - return true - } - } - - return false -} diff --git a/backend/init/viper/viper.go b/backend/init/viper/viper.go deleted file mode 100644 index 084a7d84d..000000000 --- a/backend/init/viper/viper.go +++ /dev/null @@ -1,90 +0,0 @@ -package viper - -import ( - "bytes" - "fmt" - "path" - "strings" - - "github.com/1Panel-dev/1Panel/backend/configs" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/1Panel-dev/1Panel/cmd/server/conf" - "github.com/fsnotify/fsnotify" - "github.com/spf13/viper" - "gopkg.in/yaml.v3" -) - -func Init() { - baseDir := "/opt" - mode := "" - version := "v1.0.0" - fileOp := files.NewFileOp() - v := viper.NewWithOptions() - v.SetConfigType("yaml") - - config := configs.ServerConfig{} - if err := yaml.Unmarshal(conf.AppYaml, &config); err != nil { - panic(err) - } - if config.System.Mode != "" { - mode = config.System.Mode - } - if mode == "dev" && fileOp.Stat("/opt/1panel/conf/app.yaml") { - v.SetConfigName("app") - v.AddConfigPath(path.Join("/opt/1panel/conf")) - if err := v.ReadInConfig(); err != nil { - panic(fmt.Errorf("Fatal error config file: %s \n", err)) - } - } else { - baseDir = loadParams("BASE_DIR") - version = loadParams("ORIGINAL_VERSION") - - reader := bytes.NewReader(conf.AppYaml) - if err := v.ReadConfig(reader); err != nil { - panic(fmt.Errorf("Fatal error config file: %s \n", err)) - } - } - v.OnConfigChange(func(e fsnotify.Event) { - if err := v.Unmarshal(&global.CONF); err != nil { - panic(err) - } - }) - serverConfig := configs.ServerConfig{} - if err := v.Unmarshal(&serverConfig); err != nil { - panic(err) - } - if mode == "dev" && fileOp.Stat("/opt/1panel/conf/app.yaml") { - if serverConfig.System.BaseDir != "" { - baseDir = serverConfig.System.BaseDir - } - if serverConfig.System.Version != "" { - version = serverConfig.System.Version - } - } - - global.CONF = serverConfig - global.CONF.System.BaseDir = baseDir - global.CONF.System.IsDemo = v.GetBool("system.is_demo") - global.CONF.System.DataDir = path.Join(global.CONF.System.BaseDir, "1panel") - global.CONF.System.Cache = path.Join(global.CONF.System.DataDir, "cache") - global.CONF.System.Backup = path.Join(global.CONF.System.DataDir, "backup") - global.CONF.System.DbPath = path.Join(global.CONF.System.DataDir, "db") - global.CONF.System.LogPath = path.Join(global.CONF.System.DataDir, "log") - global.CONF.System.TmpDir = path.Join(global.CONF.System.DataDir, "tmp") - global.CONF.System.Version = version - global.Viper = v -} - -func loadParams(param string) string { - stdout, err := cmd.Execf("grep '^%s=' /usr/local/bin/1pctl | cut -d'=' -f2", param) - if err != nil { - panic(err) - } - info := strings.ReplaceAll(stdout, "\n", "") - if len(info) == 0 || info == `""` { - panic(fmt.Sprintf("error `%s` find in /usr/local/bin/1pctl", param)) - } - return info -} diff --git a/backend/log/config.go b/backend/log/config.go deleted file mode 100644 index 08c1d6173..000000000 --- a/backend/log/config.go +++ /dev/null @@ -1,41 +0,0 @@ -package log - -import ( - "errors" - "io" - "os" - "path" -) - -var ( - BufferSize = 0x100000 - DefaultFileMode = os.FileMode(0644) - DefaultFileFlag = os.O_RDWR | os.O_CREATE | os.O_APPEND - ErrInvalidArgument = errors.New("error argument invalid") - QueueSize = 1024 - ErrClosed = errors.New("error write on close") -) - -type Config struct { - TimeTagFormat string - LogPath string - FileName string - LogSuffix string - MaxRemain int - RollingTimePattern string -} - -type Manager interface { - Fire() chan string - Close() -} - -type RollingWriter interface { - io.Writer - Close() error -} - -func FilePath(c *Config) (filepath string) { - filepath = path.Join(c.LogPath, c.FileName) + c.LogSuffix - return -} diff --git a/backend/log/dup_write_darwin.go b/backend/log/dup_write_darwin.go deleted file mode 100644 index 822d24e82..000000000 --- a/backend/log/dup_write_darwin.go +++ /dev/null @@ -1,20 +0,0 @@ -package log - -import ( - "golang.org/x/sys/unix" - "os" - "runtime" -) - -var stdErrFileHandler *os.File - -func dupWrite(file *os.File) error { - stdErrFileHandler = file - if err := unix.Dup2(int(file.Fd()), int(os.Stderr.Fd())); err != nil { - return err - } - runtime.SetFinalizer(stdErrFileHandler, func(fd *os.File) { - fd.Close() - }) - return nil -} diff --git a/backend/log/dup_write_linux.go b/backend/log/dup_write_linux.go deleted file mode 100644 index 822d24e82..000000000 --- a/backend/log/dup_write_linux.go +++ /dev/null @@ -1,20 +0,0 @@ -package log - -import ( - "golang.org/x/sys/unix" - "os" - "runtime" -) - -var stdErrFileHandler *os.File - -func dupWrite(file *os.File) error { - stdErrFileHandler = file - if err := unix.Dup2(int(file.Fd()), int(os.Stderr.Fd())); err != nil { - return err - } - runtime.SetFinalizer(stdErrFileHandler, func(fd *os.File) { - fd.Close() - }) - return nil -} diff --git a/backend/log/dup_write_windows.go b/backend/log/dup_write_windows.go deleted file mode 100644 index 4996cc12d..000000000 --- a/backend/log/dup_write_windows.go +++ /dev/null @@ -1,9 +0,0 @@ -package log - -import ( - "os" -) - -func dupWrite(file *os.File) error { - return nil -} diff --git a/backend/log/manager.go b/backend/log/manager.go deleted file mode 100644 index 1f4f94546..000000000 --- a/backend/log/manager.go +++ /dev/null @@ -1,53 +0,0 @@ -package log - -import ( - "github.com/robfig/cron/v3" - "path" - "sync" - "time" -) - -type manager struct { - startAt time.Time - fire chan string - cr *cron.Cron - context chan int - wg sync.WaitGroup - lock sync.Mutex -} - -func (m *manager) Fire() chan string { - return m.fire -} - -func (m *manager) Close() { - close(m.context) - m.cr.Stop() -} - -func NewManager(c *Config) (Manager, error) { - m := &manager{ - startAt: time.Now(), - cr: cron.New(), - fire: make(chan string), - context: make(chan int), - wg: sync.WaitGroup{}, - } - - if _, err := m.cr.AddFunc(c.RollingTimePattern, func() { - m.fire <- m.GenLogFileName(c) - }); err != nil { - return nil, err - } - m.cr.Start() - - return m, nil -} - -func (m *manager) GenLogFileName(c *Config) (filename string) { - m.lock.Lock() - filename = path.Join(c.LogPath, c.FileName+"-"+m.startAt.Format(c.TimeTagFormat)) + c.LogSuffix - m.startAt = time.Now() - m.lock.Unlock() - return -} diff --git a/backend/log/writer.go b/backend/log/writer.go deleted file mode 100644 index 74d7a1959..000000000 --- a/backend/log/writer.go +++ /dev/null @@ -1,250 +0,0 @@ -package log - -import ( - "log" - "os" - "path" - "sort" - "strings" - "sync" - "sync/atomic" - "time" - "unsafe" - - "github.com/1Panel-dev/1Panel/backend/global" -) - -type Writer struct { - m Manager - file *os.File - absPath string - fire chan string - cf *Config - rollingfilech chan string -} - -type AsynchronousWriter struct { - Writer - ctx chan int - queue chan []byte - errChan chan error - closed int32 - wg sync.WaitGroup -} - -func (w *AsynchronousWriter) Close() error { - if atomic.CompareAndSwapInt32(&w.closed, 0, 1) { - close(w.ctx) - w.onClose() - - func() { - defer func() { - if r := recover(); r != nil { - global.LOG.Error(r) - } - }() - w.m.Close() - }() - return w.file.Close() - } - return ErrClosed -} - -func (w *AsynchronousWriter) onClose() { - var err error - for { - select { - case b := <-w.queue: - if _, err = w.file.Write(b); err != nil { - select { - case w.errChan <- err: - default: - _asyncBufferPool.Put(&b) - return - } - } - _asyncBufferPool.Put(&b) - default: - return - } - } -} - -var _asyncBufferPool = sync.Pool{ - New: func() interface{} { - return make([]byte, BufferSize) - }, -} - -func NewWriterFromConfig(c *Config) (RollingWriter, error) { - if c.LogPath == "" || c.FileName == "" { - return nil, ErrInvalidArgument - } - if err := os.MkdirAll(c.LogPath, 0700); err != nil { - return nil, err - } - filepath := FilePath(c) - file, err := os.OpenFile(filepath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) - if err != nil { - return nil, err - } - if err := dupWrite(file); err != nil { - return nil, err - } - mng, err := NewManager(c) - if err != nil { - return nil, err - } - - var rollingWriter RollingWriter - writer := Writer{ - m: mng, - file: file, - absPath: filepath, - fire: mng.Fire(), - cf: c, - } - if c.MaxRemain > 0 { - writer.rollingfilech = make(chan string, c.MaxRemain) - dir, err := os.ReadDir(c.LogPath) - if err != nil { - mng.Close() - return nil, err - } - - files := make([]string, 0, 10) - for _, fi := range dir { - if fi.IsDir() { - continue - } - - fileName := c.FileName - if strings.Contains(fi.Name(), fileName) && strings.Contains(fi.Name(), c.LogSuffix) { - start := strings.Index(fi.Name(), "-") - end := strings.Index(fi.Name(), c.LogSuffix) - name := fi.Name() - if start > 0 && end > 0 { - _, err := time.Parse(c.TimeTagFormat, name[start+1:end]) - if err == nil { - files = append(files, fi.Name()) - } - } - } - } - sort.Slice(files, func(i, j int) bool { - t1Start := strings.Index(files[i], "-") - t1End := strings.Index(files[i], c.LogSuffix) - t2Start := strings.Index(files[i], "-") - t2End := strings.Index(files[i], c.LogSuffix) - t1, _ := time.Parse(c.TimeTagFormat, files[i][t1Start+1:t1End]) - t2, _ := time.Parse(c.TimeTagFormat, files[j][t2Start+1:t2End]) - return t1.Before(t2) - }) - - for _, file := range files { - retry: - select { - case writer.rollingfilech <- path.Join(c.LogPath, file): - default: - writer.DoRemove() - goto retry - } - } - } - - wr := &AsynchronousWriter{ - ctx: make(chan int), - queue: make(chan []byte, QueueSize), - errChan: make(chan error, QueueSize), - wg: sync.WaitGroup{}, - closed: 0, - Writer: writer, - } - - wr.wg.Add(1) - go wr.writer() - wr.wg.Wait() - rollingWriter = wr - - return rollingWriter, nil -} - -func (w *AsynchronousWriter) writer() { - var err error - w.wg.Done() - for { - select { - case filename := <-w.fire: - if err = w.Reopen(filename); err != nil && len(w.errChan) < cap(w.errChan) { - w.errChan <- err - } - case b := <-w.queue: - if _, err = w.file.Write(b); err != nil && len(w.errChan) < cap(w.errChan) { - w.errChan <- err - } - _asyncBufferPool.Put(&b) - case <-w.ctx: - return - } - } -} - -func (w *Writer) DoRemove() { - file := <-w.rollingfilech - if err := os.Remove(file); err != nil { - log.Println("error in remove log file", file, err) - } -} - -func (w *Writer) Write(b []byte) (int, error) { - var ok = false - for !ok { - select { - case filename := <-w.fire: - if err := w.Reopen(filename); err != nil { - return 0, err - } - default: - ok = true - } - } - - fp := atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&w.file))) - file := (*os.File)(fp) - return file.Write(b) -} - -func (w *Writer) Reopen(file string) error { - fileInfo, err := w.file.Stat() - if err != nil { - return err - } - - if fileInfo.Size() == 0 { - return nil - } - - w.file.Close() - if err := os.Rename(w.absPath, file); err != nil { - return err - } - newFile, err := os.OpenFile(w.absPath, DefaultFileFlag, DefaultFileMode) - if err != nil { - return err - } - - w.file = newFile - - go func() { - if w.cf.MaxRemain > 0 { - retry: - select { - case w.rollingfilech <- file: - default: - w.DoRemove() - goto retry - } - } - }() - return nil -} diff --git a/backend/middleware/demo_handle.go b/backend/middleware/demo_handle.go deleted file mode 100644 index b18240ec2..000000000 --- a/backend/middleware/demo_handle.go +++ /dev/null @@ -1,57 +0,0 @@ -package middleware - -import ( - "net/http" - "strings" - - "github.com/1Panel-dev/1Panel/backend/app/dto" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -var whiteUrlList = map[string]struct{}{ - "/api/v1/auth/login": {}, - "/api/v1/websites/config": {}, - "/api/v1/websites/waf/config": {}, - "/api/v1/files/loadfile": {}, - "/api/v1/files/size": {}, - "/api/v1/logs/operation": {}, - "/api/v1/logs/login": {}, - "/api/v1/auth/logout": {}, - - "/api/v1/apps/installed/loadport": {}, - "/api/v1/apps/installed/check": {}, - "/api/v1/apps/installed/conninfo": {}, - "/api/v1/databases/load/file": {}, - "/api/v1/databases/variables": {}, - "/api/v1/databases/status": {}, - "/api/v1/databases/baseinfo": {}, - - "/api/v1/waf/attack/stat": {}, - "/api/v1/waf/config/website": {}, - - "/api/v1/monitor/stat": {}, - "/api/v1/monitor/visitors": {}, - "/api/v1/monitor/visitors/loc": {}, - "/api/v1/monitor/qps": {}, -} - -func DemoHandle() gin.HandlerFunc { - return func(c *gin.Context) { - if strings.Contains(c.Request.URL.Path, "search") || c.Request.Method == http.MethodGet { - c.Next() - return - } - if _, ok := whiteUrlList[c.Request.URL.Path]; ok { - c.Next() - return - } - - c.JSON(http.StatusInternalServerError, dto.Response{ - Code: http.StatusInternalServerError, - Message: buserr.New(constant.ErrDemoEnvironment).Error(), - }) - c.Abort() - } -} diff --git a/backend/middleware/helper.go b/backend/middleware/helper.go deleted file mode 100644 index b25c99907..000000000 --- a/backend/middleware/helper.go +++ /dev/null @@ -1,32 +0,0 @@ -package middleware - -import ( - "net/http" - - "github.com/1Panel-dev/1Panel/backend/app/repo" -) - -func LoadErrCode(errInfo string) int { - settingRepo := repo.NewISettingRepo() - codeVal, err := settingRepo.Get(settingRepo.WithByKey("NoAuthSetting")) - if err != nil { - return 500 - } - - switch codeVal.Value { - case "400": - return http.StatusBadRequest - case "401": - return http.StatusUnauthorized - case "403": - return http.StatusForbidden - case "404": - return http.StatusNotFound - case "408": - return http.StatusRequestTimeout - case "416": - return http.StatusRequestedRangeNotSatisfiable - default: - return http.StatusOK - } -} diff --git a/backend/middleware/loading.go b/backend/middleware/loading.go deleted file mode 100644 index eccb30931..000000000 --- a/backend/middleware/loading.go +++ /dev/null @@ -1,24 +0,0 @@ -package middleware - -import ( - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" - "github.com/1Panel-dev/1Panel/backend/app/repo" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/gin-gonic/gin" -) - -func GlobalLoading() gin.HandlerFunc { - return func(c *gin.Context) { - settingRepo := repo.NewISettingRepo() - status, err := settingRepo.Get(settingRepo.WithByKey("SystemStatus")) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - if status.Value != "Free" { - helper.ErrorWithDetail(c, constant.CodeGlobalLoading, status.Value, err) - return - } - c.Next() - } -} diff --git a/backend/router/common.go b/backend/router/common.go deleted file mode 100644 index b96153767..000000000 --- a/backend/router/common.go +++ /dev/null @@ -1,26 +0,0 @@ -package router - -func commonGroups() []CommonRouter { - return []CommonRouter{ - &DashboardRouter{}, - &HostRouter{}, - &ContainerRouter{}, - &LogRouter{}, - &FileRouter{}, - &ToolboxRouter{}, - &TerminalRouter{}, - &CronjobRouter{}, - &SettingRouter{}, - &AppRouter{}, - &WebsiteRouter{}, - &WebsiteGroupRouter{}, - &WebsiteDnsAccountRouter{}, - &WebsiteAcmeAccountRouter{}, - &WebsiteSSLRouter{}, - &DatabaseRouter{}, - &NginxRouter{}, - &RuntimeRouter{}, - &ProcessRouter{}, - &WebsiteCARouter{}, - } -} diff --git a/backend/router/entry.go b/backend/router/entry.go deleted file mode 100644 index 49ed25fd1..000000000 --- a/backend/router/entry.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !xpack - -package router - -func RouterGroups() []CommonRouter { - return commonGroups() -} - -var RouterGroupApp = RouterGroups() diff --git a/backend/router/ro_app.go b/backend/router/ro_app.go deleted file mode 100644 index 12877f54c..000000000 --- a/backend/router/ro_app.go +++ /dev/null @@ -1,41 +0,0 @@ -package router - -import ( - v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1" - "github.com/gin-gonic/gin" -) - -type AppRouter struct { -} - -func (a *AppRouter) InitRouter(Router *gin.RouterGroup) { - appRouter := Router.Group("apps") - - baseApi := v1.ApiGroupApp.BaseApi - { - appRouter.POST("/sync", baseApi.SyncApp) - appRouter.GET("/checkupdate", baseApi.GetAppListUpdate) - appRouter.POST("/search", baseApi.SearchApp) - appRouter.GET("/:key", baseApi.GetApp) - appRouter.GET("/detail/:appId/:version/:type", baseApi.GetAppDetail) - appRouter.GET("/details/:id", baseApi.GetAppDetailByID) - appRouter.POST("/install", baseApi.InstallApp) - appRouter.GET("/tags", baseApi.GetAppTags) - appRouter.POST("/installed/check", baseApi.CheckAppInstalled) - appRouter.POST("/installed/loadport", baseApi.LoadPort) - appRouter.POST("/installed/conninfo", baseApi.LoadConnInfo) - appRouter.GET("/installed/delete/check/:appInstallId", baseApi.DeleteCheck) - appRouter.POST("/installed/search", baseApi.SearchAppInstalled) - appRouter.GET("/installed/list", baseApi.ListAppInstalled) - appRouter.POST("/installed/op", baseApi.OperateInstalled) - appRouter.POST("/installed/sync", baseApi.SyncInstalled) - appRouter.POST("/installed/port/change", baseApi.ChangeAppPort) - appRouter.GET("/services/:key", baseApi.GetServices) - appRouter.POST("/installed/conf", baseApi.GetDefaultConfig) - appRouter.GET("/installed/params/:appInstallId", baseApi.GetParams) - appRouter.POST("/installed/params/update", baseApi.UpdateInstalled) - appRouter.POST("/installed/ignore", baseApi.IgnoreUpgrade) - appRouter.GET("/ignored/detail", baseApi.GetIgnoredApp) - appRouter.POST("/installed/update/versions", baseApi.GetUpdateVersions) - } -} diff --git a/backend/router/ro_container.go b/backend/router/ro_container.go deleted file mode 100644 index 33e591851..000000000 --- a/backend/router/ro_container.go +++ /dev/null @@ -1,84 +0,0 @@ -package router - -import ( - v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1" - "github.com/gin-gonic/gin" -) - -type ContainerRouter struct{} - -func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) { - baRouter := Router.Group("containers") - baseApi := v1.ApiGroupApp.BaseApi - { - baRouter.GET("/exec", baseApi.ContainerWsSsh) - baRouter.GET("/stats/:id", baseApi.ContainerStats) - - baRouter.POST("", baseApi.ContainerCreate) - baRouter.POST("/update", baseApi.ContainerUpdate) - baRouter.POST("/upgrade", baseApi.ContainerUpgrade) - baRouter.POST("/info", baseApi.ContainerInfo) - baRouter.POST("/search", baseApi.SearchContainer) - baRouter.POST("/list", baseApi.ListContainer) - baRouter.GET("/list/stats", baseApi.ContainerListStats) - baRouter.GET("/search/log", baseApi.ContainerLogs) - baRouter.POST("/download/log", baseApi.DownloadContainerLogs) - baRouter.GET("/limit", baseApi.LoadResourceLimit) - baRouter.POST("/clean/log", baseApi.CleanContainerLog) - baRouter.POST("/load/log", baseApi.LoadContainerLog) - baRouter.POST("/inspect", baseApi.Inspect) - baRouter.POST("/rename", baseApi.ContainerRename) - baRouter.POST("/commit", baseApi.ContainerCommit) - baRouter.POST("/operate", baseApi.ContainerOperation) - baRouter.POST("/prune", baseApi.ContainerPrune) - - baRouter.GET("/repo", baseApi.ListRepo) - baRouter.POST("/repo/status", baseApi.CheckRepoStatus) - baRouter.POST("/repo/search", baseApi.SearchRepo) - baRouter.POST("/repo/update", baseApi.UpdateRepo) - baRouter.POST("/repo", baseApi.CreateRepo) - baRouter.POST("/repo/del", baseApi.DeleteRepo) - - baRouter.POST("/compose/search", baseApi.SearchCompose) - baRouter.POST("/compose", baseApi.CreateCompose) - baRouter.POST("/compose/test", baseApi.TestCompose) - baRouter.POST("/compose/operate", baseApi.OperatorCompose) - baRouter.POST("/compose/update", baseApi.ComposeUpdate) - baRouter.GET("/compose/search/log", baseApi.ComposeLogs) - - baRouter.GET("/template", baseApi.ListComposeTemplate) - baRouter.POST("/template/search", baseApi.SearchComposeTemplate) - baRouter.POST("/template/update", baseApi.UpdateComposeTemplate) - baRouter.POST("/template", baseApi.CreateComposeTemplate) - baRouter.POST("/template/del", baseApi.DeleteComposeTemplate) - - baRouter.GET("/image", baseApi.ListImage) - baRouter.GET("/image/all", baseApi.ListAllImage) - baRouter.POST("/image/search", baseApi.SearchImage) - baRouter.POST("/image/pull", baseApi.ImagePull) - baRouter.POST("/image/push", baseApi.ImagePush) - baRouter.POST("/image/save", baseApi.ImageSave) - baRouter.POST("/image/load", baseApi.ImageLoad) - baRouter.POST("/image/remove", baseApi.ImageRemove) - baRouter.POST("/image/tag", baseApi.ImageTag) - baRouter.POST("/image/build", baseApi.ImageBuild) - - baRouter.GET("/network", baseApi.ListNetwork) - baRouter.POST("/network/del", baseApi.DeleteNetwork) - baRouter.POST("/network/search", baseApi.SearchNetwork) - baRouter.POST("/network", baseApi.CreateNetwork) - baRouter.GET("/volume", baseApi.ListVolume) - baRouter.POST("/volume/del", baseApi.DeleteVolume) - baRouter.POST("/volume/search", baseApi.SearchVolume) - baRouter.POST("/volume", baseApi.CreateVolume) - - baRouter.GET("/daemonjson", baseApi.LoadDaemonJson) - baRouter.GET("/daemonjson/file", baseApi.LoadDaemonJsonFile) - baRouter.GET("/docker/status", baseApi.LoadDockerStatus) - baRouter.POST("/docker/operate", baseApi.OperateDocker) - baRouter.POST("/daemonjson/update", baseApi.UpdateDaemonJson) - baRouter.POST("/logoption/update", baseApi.UpdateLogOption) - baRouter.POST("/ipv6option/update", baseApi.UpdateIpv6Option) - baRouter.POST("/daemonjson/update/byfile", baseApi.UpdateDaemonJsonByFile) - } -} diff --git a/backend/router/ro_cronjob.go b/backend/router/ro_cronjob.go deleted file mode 100644 index 672ff36a5..000000000 --- a/backend/router/ro_cronjob.go +++ /dev/null @@ -1,26 +0,0 @@ -package router - -import ( - v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1" - - "github.com/gin-gonic/gin" -) - -type CronjobRouter struct{} - -func (s *CronjobRouter) InitRouter(Router *gin.RouterGroup) { - cmdRouter := Router.Group("cronjobs") - baseApi := v1.ApiGroupApp.BaseApi - { - cmdRouter.POST("", baseApi.CreateCronjob) - cmdRouter.POST("/del", baseApi.DeleteCronjob) - cmdRouter.POST("/update", baseApi.UpdateCronjob) - cmdRouter.POST("/status", baseApi.UpdateCronjobStatus) - cmdRouter.POST("/handle", baseApi.HandleOnce) - cmdRouter.POST("/download", baseApi.TargetDownload) - cmdRouter.POST("/search", baseApi.SearchCronjob) - cmdRouter.POST("/search/records", baseApi.SearchJobRecords) - cmdRouter.POST("/records/log", baseApi.LoadRecordLog) - cmdRouter.POST("/records/clean", baseApi.CleanRecord) - } -} diff --git a/backend/router/ro_dashboard.go b/backend/router/ro_dashboard.go deleted file mode 100644 index 1445e6180..000000000 --- a/backend/router/ro_dashboard.go +++ /dev/null @@ -1,20 +0,0 @@ -package router - -import ( - v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1" - - "github.com/gin-gonic/gin" -) - -type DashboardRouter struct{} - -func (s *DashboardRouter) InitRouter(Router *gin.RouterGroup) { - cmdRouter := Router.Group("dashboard") - baseApi := v1.ApiGroupApp.BaseApi - { - cmdRouter.GET("/base/os", baseApi.LoadDashboardOsInfo) - cmdRouter.GET("/base/:ioOption/:netOption", baseApi.LoadDashboardBaseInfo) - cmdRouter.GET("/current/:ioOption/:netOption", baseApi.LoadDashboardCurrentInfo) - cmdRouter.POST("/system/restart/:operation", baseApi.SystemRestart) - } -} diff --git a/backend/router/ro_database.go b/backend/router/ro_database.go deleted file mode 100644 index e2dd4e016..000000000 --- a/backend/router/ro_database.go +++ /dev/null @@ -1,64 +0,0 @@ -package router - -import ( - v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1" - - "github.com/gin-gonic/gin" -) - -type DatabaseRouter struct{} - -func (s *DatabaseRouter) InitRouter(Router *gin.RouterGroup) { - cmdRouter := Router.Group("databases") - baseApi := v1.ApiGroupApp.BaseApi - { - cmdRouter.POST("/common/info", baseApi.LoadDBBaseInfo) - cmdRouter.POST("/common/load/file", baseApi.LoadDBFile) - cmdRouter.POST("/common/update/conf", baseApi.UpdateDBConfByFile) - - cmdRouter.POST("", baseApi.CreateMysql) - cmdRouter.POST("/bind", baseApi.BindUser) - cmdRouter.POST("load", baseApi.LoadDBFromRemote) - cmdRouter.POST("/change/access", baseApi.ChangeMysqlAccess) - cmdRouter.POST("/change/password", baseApi.ChangeMysqlPassword) - cmdRouter.POST("/del/check", baseApi.DeleteCheckMysql) - cmdRouter.POST("/del", baseApi.DeleteMysql) - cmdRouter.POST("/description/update", baseApi.UpdateMysqlDescription) - cmdRouter.POST("/variables/update", baseApi.UpdateMysqlVariables) - cmdRouter.POST("/search", baseApi.SearchMysql) - cmdRouter.POST("/variables", baseApi.LoadVariables) - cmdRouter.POST("/status", baseApi.LoadStatus) - cmdRouter.POST("/remote", baseApi.LoadRemoteAccess) - cmdRouter.GET("/options", baseApi.ListDBName) - - cmdRouter.POST("/redis/persistence/conf", baseApi.LoadPersistenceConf) - cmdRouter.POST("/redis/status", baseApi.LoadRedisStatus) - cmdRouter.POST("/redis/conf", baseApi.LoadRedisConf) - cmdRouter.GET("/redis/exec", baseApi.RedisWsSsh) - cmdRouter.GET("/redis/check", baseApi.CheckHasCli) - cmdRouter.POST("/redis/install/cli", baseApi.InstallCli) - cmdRouter.POST("/redis/password", baseApi.ChangeRedisPassword) - cmdRouter.POST("/redis/conf/update", baseApi.UpdateRedisConf) - cmdRouter.POST("/redis/persistence/update", baseApi.UpdateRedisPersistenceConf) - - cmdRouter.POST("/db/check", baseApi.CheckDatabase) - cmdRouter.POST("/db", baseApi.CreateDatabase) - cmdRouter.GET("/db/:name", baseApi.GetDatabase) - cmdRouter.GET("/db/list/:type", baseApi.ListDatabase) - cmdRouter.GET("/db/item/:type", baseApi.LoadDatabaseItems) - cmdRouter.POST("/db/update", baseApi.UpdateDatabase) - cmdRouter.POST("/db/search", baseApi.SearchDatabase) - cmdRouter.POST("/db/del/check", baseApi.DeleteCheckDatabase) - cmdRouter.POST("/db/del", baseApi.DeleteDatabase) - - cmdRouter.POST("/pg", baseApi.CreatePostgresql) - cmdRouter.POST("/pg/search", baseApi.SearchPostgresql) - cmdRouter.POST("/pg/:database/load", baseApi.LoadPostgresqlDBFromRemote) - cmdRouter.POST("/pg/bind", baseApi.BindPostgresqlUser) - cmdRouter.POST("/pg/del/check", baseApi.DeleteCheckPostgresql) - cmdRouter.POST("/pg/del", baseApi.DeletePostgresql) - cmdRouter.POST("/pg/privileges", baseApi.ChangePostgresqlPrivileges) - cmdRouter.POST("/pg/password", baseApi.ChangePostgresqlPassword) - cmdRouter.POST("/pg/description", baseApi.UpdatePostgresqlDescription) - } -} diff --git a/backend/router/ro_file.go b/backend/router/ro_file.go deleted file mode 100644 index 78fba28ce..000000000 --- a/backend/router/ro_file.go +++ /dev/null @@ -1,51 +0,0 @@ -package router - -import ( - v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1" - "github.com/gin-gonic/gin" -) - -type FileRouter struct { -} - -func (f *FileRouter) InitRouter(Router *gin.RouterGroup) { - fileRouter := Router.Group("files") - baseApi := v1.ApiGroupApp.BaseApi - { - fileRouter.POST("/search", baseApi.ListFiles) - fileRouter.POST("/upload/search", baseApi.SearchUploadWithPage) - fileRouter.POST("/tree", baseApi.GetFileTree) - fileRouter.POST("", baseApi.CreateFile) - fileRouter.POST("/del", baseApi.DeleteFile) - fileRouter.POST("/batch/del", baseApi.BatchDeleteFile) - fileRouter.POST("/mode", baseApi.ChangeFileMode) - fileRouter.POST("/owner", baseApi.ChangeFileOwner) - fileRouter.POST("/compress", baseApi.CompressFile) - fileRouter.POST("/decompress", baseApi.DeCompressFile) - fileRouter.POST("/content", baseApi.GetContent) - fileRouter.POST("/save", baseApi.SaveContent) - fileRouter.POST("/check", baseApi.CheckFile) - fileRouter.POST("/upload", baseApi.UploadFiles) - fileRouter.POST("/chunkupload", baseApi.UploadChunkFiles) - fileRouter.POST("/rename", baseApi.ChangeFileName) - fileRouter.POST("/wget", baseApi.WgetFile) - fileRouter.POST("/move", baseApi.MoveFile) - fileRouter.GET("/download", baseApi.Download) - fileRouter.POST("/chunkdownload", baseApi.DownloadChunkFiles) - fileRouter.POST("/size", baseApi.Size) - fileRouter.GET("/ws", baseApi.Ws) - fileRouter.GET("/keys", baseApi.Keys) - fileRouter.POST("/read", baseApi.ReadFileByLine) - fileRouter.POST("/batch/role", baseApi.BatchChangeModeAndOwner) - - fileRouter.POST("/recycle/search", baseApi.SearchRecycleBinFile) - fileRouter.POST("/recycle/reduce", baseApi.ReduceRecycleBinFile) - fileRouter.POST("/recycle/clear", baseApi.ClearRecycleBinFile) - fileRouter.GET("/recycle/status", baseApi.GetRecycleStatus) - - fileRouter.POST("/favorite/search", baseApi.SearchFavorite) - fileRouter.POST("/favorite", baseApi.CreateFavorite) - fileRouter.POST("/favorite/del", baseApi.DeleteFavorite) - - } -} diff --git a/backend/router/ro_group.go b/backend/router/ro_group.go deleted file mode 100644 index aefcdee40..000000000 --- a/backend/router/ro_group.go +++ /dev/null @@ -1,21 +0,0 @@ -package router - -import ( - v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1" - "github.com/gin-gonic/gin" -) - -type WebsiteGroupRouter struct { -} - -func (a *WebsiteGroupRouter) InitRouter(Router *gin.RouterGroup) { - groupRouter := Router.Group("groups") - - baseApi := v1.ApiGroupApp.BaseApi - { - groupRouter.POST("", baseApi.CreateGroup) - groupRouter.POST("/del", baseApi.DeleteGroup) - groupRouter.POST("/update", baseApi.UpdateGroup) - groupRouter.POST("/search", baseApi.ListGroup) - } -} diff --git a/backend/router/ro_host.go b/backend/router/ro_host.go deleted file mode 100644 index 1405b2014..000000000 --- a/backend/router/ro_host.go +++ /dev/null @@ -1,70 +0,0 @@ -package router - -import ( - v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1" - - "github.com/gin-gonic/gin" -) - -type HostRouter struct{} - -func (s *HostRouter) InitRouter(Router *gin.RouterGroup) { - hostRouter := Router.Group("hosts") - baseApi := v1.ApiGroupApp.BaseApi - { - hostRouter.POST("", baseApi.CreateHost) - hostRouter.POST("/del", baseApi.DeleteHost) - hostRouter.POST("/update", baseApi.UpdateHost) - hostRouter.POST("/update/group", baseApi.UpdateHostGroup) - hostRouter.POST("/search", baseApi.SearchHost) - hostRouter.POST("/tree", baseApi.HostTree) - hostRouter.POST("/test/byinfo", baseApi.TestByInfo) - hostRouter.POST("/test/byid/:id", baseApi.TestByID) - - hostRouter.GET("/firewall/base", baseApi.LoadFirewallBaseInfo) - hostRouter.POST("/firewall/search", baseApi.SearchFirewallRule) - hostRouter.POST("/firewall/operate", baseApi.OperateFirewall) - hostRouter.POST("/firewall/port", baseApi.OperatePortRule) - hostRouter.POST("/firewall/forward", baseApi.OperateForwardRule) - hostRouter.POST("/firewall/ip", baseApi.OperateIPRule) - hostRouter.POST("/firewall/batch", baseApi.BatchOperateRule) - hostRouter.POST("/firewall/update/port", baseApi.UpdatePortRule) - hostRouter.POST("/firewall/update/addr", baseApi.UpdateAddrRule) - hostRouter.POST("/firewall/update/description", baseApi.UpdateFirewallDescription) - - hostRouter.POST("/monitor/search", baseApi.LoadMonitor) - hostRouter.POST("/monitor/clean", baseApi.CleanMonitor) - hostRouter.GET("/monitor/netoptions", baseApi.GetNetworkOptions) - hostRouter.GET("/monitor/iooptions", baseApi.GetIOOptions) - - hostRouter.GET("/ssh/conf", baseApi.LoadSSHConf) - hostRouter.POST("/ssh/search", baseApi.GetSSHInfo) - hostRouter.POST("/ssh/update", baseApi.UpdateSSH) - hostRouter.POST("/ssh/generate", baseApi.GenerateSSH) - hostRouter.POST("/ssh/secret", baseApi.LoadSSHSecret) - hostRouter.POST("/ssh/log", baseApi.LoadSSHLogs) - hostRouter.POST("/ssh/conffile/update", baseApi.UpdateSSHByfile) - hostRouter.POST("/ssh/operate", baseApi.OperateSSH) - - hostRouter.GET("/command", baseApi.ListCommand) - hostRouter.POST("/command", baseApi.CreateCommand) - hostRouter.POST("/command/del", baseApi.DeleteCommand) - hostRouter.POST("/command/search", baseApi.SearchCommand) - hostRouter.GET("/command/tree", baseApi.SearchCommandTree) - hostRouter.POST("/command/update", baseApi.UpdateCommand) - - hostRouter.GET("/command/redis", baseApi.ListRedisCommand) - hostRouter.POST("/command/redis", baseApi.SaveRedisCommand) - hostRouter.POST("/command/redis/search", baseApi.SearchRedisCommand) - hostRouter.POST("/command/redis/del", baseApi.DeleteRedisCommand) - - hostRouter.POST("/tool", baseApi.GetToolStatus) - hostRouter.POST("/tool/init", baseApi.InitToolConfig) - hostRouter.POST("/tool/operate", baseApi.OperateTool) - hostRouter.POST("/tool/config", baseApi.OperateToolConfig) - hostRouter.POST("/tool/log", baseApi.GetToolLog) - hostRouter.POST("/tool/supervisor/process", baseApi.OperateProcess) - hostRouter.GET("/tool/supervisor/process", baseApi.GetProcess) - hostRouter.POST("/tool/supervisor/process/file", baseApi.GetProcessFile) - } -} diff --git a/backend/router/ro_log.go b/backend/router/ro_log.go deleted file mode 100644 index 1bb372ad7..000000000 --- a/backend/router/ro_log.go +++ /dev/null @@ -1,18 +0,0 @@ -package router - -import ( - v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1" - - "github.com/gin-gonic/gin" -) - -type LogRouter struct{} - -func (s *LogRouter) InitRouter(Router *gin.RouterGroup) { - operationRouter := Router.Group("logs") - baseApi := v1.ApiGroupApp.BaseApi - { - operationRouter.GET("/system/files", baseApi.GetSystemFiles) - operationRouter.POST("/system", baseApi.GetSystemLogs) - } -} diff --git a/backend/router/ro_nginx.go b/backend/router/ro_nginx.go deleted file mode 100644 index 38bf3825a..000000000 --- a/backend/router/ro_nginx.go +++ /dev/null @@ -1,23 +0,0 @@ -package router - -import ( - v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1" - "github.com/gin-gonic/gin" -) - -type NginxRouter struct { -} - -func (a *NginxRouter) InitRouter(Router *gin.RouterGroup) { - groupRouter := Router.Group("openresty") - - baseApi := v1.ApiGroupApp.BaseApi - { - groupRouter.GET("", baseApi.GetNginx) - groupRouter.POST("/scope", baseApi.GetNginxConfigByScope) - groupRouter.POST("/update", baseApi.UpdateNginxConfigByScope) - groupRouter.GET("/status", baseApi.GetNginxStatus) - groupRouter.POST("/file", baseApi.UpdateNginxFile) - groupRouter.POST("/clear", baseApi.ClearNginxProxyCache) - } -} diff --git a/backend/router/ro_process.go b/backend/router/ro_process.go deleted file mode 100644 index d077ff8ae..000000000 --- a/backend/router/ro_process.go +++ /dev/null @@ -1,18 +0,0 @@ -package router - -import ( - v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1" - "github.com/gin-gonic/gin" -) - -type ProcessRouter struct { -} - -func (f *ProcessRouter) InitRouter(Router *gin.RouterGroup) { - processRouter := Router.Group("process") - baseApi := v1.ApiGroupApp.BaseApi - { - processRouter.GET("/ws", baseApi.ProcessWs) - processRouter.POST("/stop", baseApi.StopProcess) - } -} diff --git a/backend/router/ro_router.go b/backend/router/ro_router.go deleted file mode 100644 index 58a52dce0..000000000 --- a/backend/router/ro_router.go +++ /dev/null @@ -1,7 +0,0 @@ -package router - -import "github.com/gin-gonic/gin" - -type CommonRouter interface { - InitRouter(Router *gin.RouterGroup) -} diff --git a/backend/router/ro_runtime.go b/backend/router/ro_runtime.go deleted file mode 100644 index 6e35b89ff..000000000 --- a/backend/router/ro_runtime.go +++ /dev/null @@ -1,35 +0,0 @@ -package router - -import ( - v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1" - "github.com/gin-gonic/gin" -) - -type RuntimeRouter struct { -} - -func (r *RuntimeRouter) InitRouter(Router *gin.RouterGroup) { - groupRouter := Router.Group("runtimes") - - baseApi := v1.ApiGroupApp.BaseApi - { - groupRouter.GET("/installed/delete/check/:runTimeId", baseApi.DeleteRuntimeCheck) - groupRouter.POST("/search", baseApi.SearchRuntimes) - groupRouter.POST("", baseApi.CreateRuntime) - groupRouter.POST("/del", baseApi.DeleteRuntime) - groupRouter.POST("/update", baseApi.UpdateRuntime) - groupRouter.GET("/:id", baseApi.GetRuntime) - groupRouter.POST("/sync", baseApi.SyncStatus) - - groupRouter.POST("/node/package", baseApi.GetNodePackageRunScript) - groupRouter.POST("/operate", baseApi.OperateRuntime) - groupRouter.POST("/node/modules", baseApi.GetNodeModules) - groupRouter.POST("/node/modules/operate", baseApi.OperateNodeModules) - - groupRouter.POST("/php/extensions/search", baseApi.PagePHPExtensions) - groupRouter.POST("/php/extensions", baseApi.CreatePHPExtensions) - groupRouter.POST("/php/extensions/update", baseApi.UpdatePHPExtensions) - groupRouter.POST("/php/extensions/del", baseApi.DeletePHPExtensions) - } - -} diff --git a/backend/router/ro_setting.go b/backend/router/ro_setting.go deleted file mode 100644 index dcb3c1eab..000000000 --- a/backend/router/ro_setting.go +++ /dev/null @@ -1,45 +0,0 @@ -package router - -import ( - v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1" - "github.com/gin-gonic/gin" -) - -type SettingRouter struct{} - -func (s *SettingRouter) InitRouter(Router *gin.RouterGroup) { - settingRouter := Router.Group("settings") - baseApi := v1.ApiGroupApp.BaseApi - { - settingRouter.POST("/search", baseApi.GetSettingInfo) - settingRouter.GET("/search/available", baseApi.GetSystemAvailable) - settingRouter.POST("/update", baseApi.UpdateSetting) - - settingRouter.POST("/snapshot", baseApi.CreateSnapshot) - settingRouter.POST("/snapshot/status", baseApi.LoadSnapShotStatus) - settingRouter.POST("/snapshot/search", baseApi.SearchSnapshot) - settingRouter.POST("/snapshot/import", baseApi.ImportSnapshot) - settingRouter.POST("/snapshot/del", baseApi.DeleteSnapshot) - settingRouter.POST("/snapshot/recover", baseApi.RecoverSnapshot) - settingRouter.POST("/snapshot/rollback", baseApi.RollbackSnapshot) - settingRouter.POST("/snapshot/description/update", baseApi.UpdateSnapDescription) - - settingRouter.GET("/backup/search", baseApi.ListBackup) - settingRouter.GET("/backup/onedrive", baseApi.LoadOneDriveInfo) - settingRouter.POST("/backup/backup", baseApi.Backup) - settingRouter.POST("/backup/refresh/onedrive", baseApi.RefreshOneDriveToken) - settingRouter.POST("/backup/recover", baseApi.Recover) - settingRouter.POST("/backup/recover/byupload", baseApi.RecoverByUpload) - settingRouter.POST("/backup/search/files", baseApi.LoadFilesFromBackup) - settingRouter.POST("/backup/buckets", baseApi.ListBuckets) - settingRouter.POST("/backup", baseApi.CreateBackup) - settingRouter.POST("/backup/del", baseApi.DeleteBackup) - settingRouter.POST("/backup/update", baseApi.UpdateBackup) - settingRouter.POST("/backup/record/search", baseApi.SearchBackupRecords) - settingRouter.POST("/backup/record/search/bycronjob", baseApi.SearchBackupRecordsByCronjob) - settingRouter.POST("/backup/record/download", baseApi.DownloadRecord) - settingRouter.POST("/backup/record/del", baseApi.DeleteBackupRecord) - - settingRouter.GET("/basedir", baseApi.LoadBaseDir) - } -} diff --git a/backend/router/ro_terminal.go b/backend/router/ro_terminal.go deleted file mode 100644 index d263c70e4..000000000 --- a/backend/router/ro_terminal.go +++ /dev/null @@ -1,17 +0,0 @@ -package router - -import ( - v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1" - - "github.com/gin-gonic/gin" -) - -type TerminalRouter struct{} - -func (s *TerminalRouter) InitRouter(Router *gin.RouterGroup) { - terminalRouter := Router.Group("terminals") - baseApi := v1.ApiGroupApp.BaseApi - { - terminalRouter.GET("", baseApi.WsSsh) - } -} diff --git a/backend/router/ro_toolbox.go b/backend/router/ro_toolbox.go deleted file mode 100644 index 8487d43dd..000000000 --- a/backend/router/ro_toolbox.go +++ /dev/null @@ -1,59 +0,0 @@ -package router - -import ( - v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1" - - "github.com/gin-gonic/gin" -) - -type ToolboxRouter struct{} - -func (s *ToolboxRouter) InitRouter(Router *gin.RouterGroup) { - toolboxRouter := Router.Group("toolbox") - baseApi := v1.ApiGroupApp.BaseApi - { - toolboxRouter.POST("/device/base", baseApi.LoadDeviceBaseInfo) - toolboxRouter.GET("/device/zone/options", baseApi.LoadTimeOption) - toolboxRouter.POST("/device/update/conf", baseApi.UpdateDeviceConf) - toolboxRouter.POST("/device/update/host", baseApi.UpdateDeviceHost) - toolboxRouter.POST("/device/update/passwd", baseApi.UpdateDevicePasswd) - toolboxRouter.POST("/device/update/swap", baseApi.UpdateDeviceSwap) - toolboxRouter.POST("/device/update/byconf", baseApi.UpdateDeviceByFile) - toolboxRouter.POST("/device/check/dns", baseApi.CheckDNS) - toolboxRouter.POST("/device/conf", baseApi.LoadDeviceConf) - - toolboxRouter.POST("/scan", baseApi.ScanSystem) - toolboxRouter.POST("/clean", baseApi.SystemClean) - - toolboxRouter.GET("/fail2ban/base", baseApi.LoadFail2BanBaseInfo) - toolboxRouter.GET("/fail2ban/load/conf", baseApi.LoadFail2BanConf) - toolboxRouter.POST("/fail2ban/search", baseApi.SearchFail2Ban) - toolboxRouter.POST("/fail2ban/operate", baseApi.OperateFail2Ban) - toolboxRouter.POST("/fail2ban/operate/sshd", baseApi.OperateSSHD) - toolboxRouter.POST("/fail2ban/update", baseApi.UpdateFail2BanConf) - toolboxRouter.POST("/fail2ban/update/byconf", baseApi.UpdateFail2BanConfByFile) - - toolboxRouter.GET("/ftp/base", baseApi.LoadFtpBaseInfo) - toolboxRouter.POST("/ftp/log/search", baseApi.LoadFtpLogInfo) - toolboxRouter.POST("/ftp/operate", baseApi.OperateFtp) - toolboxRouter.POST("/ftp/search", baseApi.SearchFtp) - toolboxRouter.POST("/ftp", baseApi.CreateFtp) - toolboxRouter.POST("/ftp/update", baseApi.UpdateFtp) - toolboxRouter.POST("/ftp/del", baseApi.DeleteFtp) - toolboxRouter.POST("/ftp/sync", baseApi.SyncFtp) - - toolboxRouter.POST("/clam/search", baseApi.SearchClam) - toolboxRouter.POST("/clam/record/search", baseApi.SearchClamRecord) - toolboxRouter.POST("/clam/record/clean", baseApi.CleanClamRecord) - toolboxRouter.POST("/clam/record/log", baseApi.LoadClamRecordLog) - toolboxRouter.POST("/clam/file/search", baseApi.SearchClamFile) - toolboxRouter.POST("/clam/file/update", baseApi.UpdateFile) - toolboxRouter.POST("/clam", baseApi.CreateClam) - toolboxRouter.POST("/clam/base", baseApi.LoadClamBaseInfo) - toolboxRouter.POST("/clam/operate", baseApi.OperateClam) - toolboxRouter.POST("/clam/update", baseApi.UpdateClam) - toolboxRouter.POST("/clam/status/update", baseApi.UpdateClamStatus) - toolboxRouter.POST("/clam/del", baseApi.DeleteClam) - toolboxRouter.POST("/clam/handle", baseApi.HandleClamScan) - } -} diff --git a/backend/router/ro_website.go b/backend/router/ro_website.go deleted file mode 100644 index 0c91907b2..000000000 --- a/backend/router/ro_website.go +++ /dev/null @@ -1,69 +0,0 @@ -package router - -import ( - v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1" - "github.com/gin-gonic/gin" -) - -type WebsiteRouter struct { -} - -func (a *WebsiteRouter) InitRouter(Router *gin.RouterGroup) { - websiteRouter := Router.Group("websites") - - baseApi := v1.ApiGroupApp.BaseApi - { - websiteRouter.POST("/search", baseApi.PageWebsite) - websiteRouter.GET("/list", baseApi.GetWebsites) - websiteRouter.POST("", baseApi.CreateWebsite) - websiteRouter.POST("/operate", baseApi.OpWebsite) - websiteRouter.POST("/log", baseApi.OpWebsiteLog) - websiteRouter.POST("/check", baseApi.CreateWebsiteCheck) - websiteRouter.GET("/options", baseApi.GetWebsiteOptions) - websiteRouter.POST("/update", baseApi.UpdateWebsite) - websiteRouter.GET("/:id", baseApi.GetWebsite) - websiteRouter.POST("/del", baseApi.DeleteWebsite) - websiteRouter.POST("/default/server", baseApi.ChangeDefaultServer) - - websiteRouter.GET("/domains/:websiteId", baseApi.GetWebDomains) - websiteRouter.POST("/domains/del", baseApi.DeleteWebDomain) - websiteRouter.POST("/domains", baseApi.CreateWebDomain) - - websiteRouter.GET("/:id/config/:type", baseApi.GetWebsiteNginx) - websiteRouter.POST("/config", baseApi.GetNginxConfig) - websiteRouter.POST("/config/update", baseApi.UpdateNginxConfig) - websiteRouter.POST("/nginx/update", baseApi.UpdateWebsiteNginxConfig) - - websiteRouter.GET("/:id/https", baseApi.GetHTTPSConfig) - websiteRouter.POST("/:id/https", baseApi.UpdateHTTPSConfig) - - websiteRouter.GET("/php/config/:id", baseApi.GetWebsitePHPConfig) - websiteRouter.POST("/php/config", baseApi.UpdateWebsitePHPConfig) - websiteRouter.POST("/php/update", baseApi.UpdatePHPFile) - websiteRouter.POST("/php/version", baseApi.ChangePHPVersion) - - websiteRouter.POST("/rewrite", baseApi.GetRewriteConfig) - websiteRouter.POST("/rewrite/update", baseApi.UpdateRewriteConfig) - - websiteRouter.POST("/dir/update", baseApi.UpdateSiteDir) - websiteRouter.POST("/dir/permission", baseApi.UpdateSiteDirPermission) - websiteRouter.POST("/dir", baseApi.GetDirConfig) - - websiteRouter.POST("/proxies", baseApi.GetProxyConfig) - websiteRouter.POST("/proxies/update", baseApi.UpdateProxyConfig) - websiteRouter.POST("/proxies/file", baseApi.UpdateProxyConfigFile) - - websiteRouter.POST("/auths", baseApi.GetAuthConfig) - websiteRouter.POST("/auths/update", baseApi.UpdateAuthConfig) - - websiteRouter.POST("/leech", baseApi.GetAntiLeech) - websiteRouter.POST("/leech/update", baseApi.UpdateAntiLeech) - - websiteRouter.POST("/redirect/update", baseApi.UpdateRedirectConfig) - websiteRouter.POST("/redirect", baseApi.GetRedirectConfig) - websiteRouter.POST("/redirect/file", baseApi.UpdateRedirectConfigFile) - - websiteRouter.GET("/default/html/:type", baseApi.GetDefaultHtml) - websiteRouter.POST("/default/html/update", baseApi.UpdateDefaultHtml) - } -} diff --git a/backend/router/ro_website_acme_account.go b/backend/router/ro_website_acme_account.go deleted file mode 100644 index f620d32d8..000000000 --- a/backend/router/ro_website_acme_account.go +++ /dev/null @@ -1,20 +0,0 @@ -package router - -import ( - v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1" - "github.com/gin-gonic/gin" -) - -type WebsiteAcmeAccountRouter struct { -} - -func (a *WebsiteAcmeAccountRouter) InitRouter(Router *gin.RouterGroup) { - groupRouter := Router.Group("websites/acme") - - baseApi := v1.ApiGroupApp.BaseApi - { - groupRouter.POST("/search", baseApi.PageWebsiteAcmeAccount) - groupRouter.POST("", baseApi.CreateWebsiteAcmeAccount) - groupRouter.POST("/del", baseApi.DeleteWebsiteAcmeAccount) - } -} diff --git a/backend/router/ro_website_ca.go b/backend/router/ro_website_ca.go deleted file mode 100644 index 5fbcc9f9c..000000000 --- a/backend/router/ro_website_ca.go +++ /dev/null @@ -1,24 +0,0 @@ -package router - -import ( - v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1" - "github.com/gin-gonic/gin" -) - -type WebsiteCARouter struct { -} - -func (a *WebsiteCARouter) InitRouter(Router *gin.RouterGroup) { - groupRouter := Router.Group("websites/ca") - - baseApi := v1.ApiGroupApp.BaseApi - { - groupRouter.POST("/search", baseApi.PageWebsiteCA) - groupRouter.POST("", baseApi.CreateWebsiteCA) - groupRouter.POST("/del", baseApi.DeleteWebsiteCA) - groupRouter.POST("/obtain", baseApi.ObtainWebsiteCA) - groupRouter.POST("/renew", baseApi.RenewWebsiteCA) - groupRouter.GET("/:id", baseApi.GetWebsiteCA) - groupRouter.POST("/download", baseApi.DownloadCAFile) - } -} diff --git a/backend/router/ro_website_dns_account.go b/backend/router/ro_website_dns_account.go deleted file mode 100644 index 8416df5c2..000000000 --- a/backend/router/ro_website_dns_account.go +++ /dev/null @@ -1,21 +0,0 @@ -package router - -import ( - v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1" - "github.com/gin-gonic/gin" -) - -type WebsiteDnsAccountRouter struct { -} - -func (a *WebsiteDnsAccountRouter) InitRouter(Router *gin.RouterGroup) { - groupRouter := Router.Group("websites/dns") - - baseApi := v1.ApiGroupApp.BaseApi - { - groupRouter.POST("/search", baseApi.PageWebsiteDnsAccount) - groupRouter.POST("", baseApi.CreateWebsiteDnsAccount) - groupRouter.POST("/update", baseApi.UpdateWebsiteDnsAccount) - groupRouter.POST("/del", baseApi.DeleteWebsiteDnsAccount) - } -} diff --git a/backend/router/ro_website_ssl.go b/backend/router/ro_website_ssl.go deleted file mode 100644 index 5664d39e0..000000000 --- a/backend/router/ro_website_ssl.go +++ /dev/null @@ -1,27 +0,0 @@ -package router - -import ( - v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1" - "github.com/gin-gonic/gin" -) - -type WebsiteSSLRouter struct { -} - -func (a *WebsiteSSLRouter) InitRouter(Router *gin.RouterGroup) { - groupRouter := Router.Group("websites/ssl") - - baseApi := v1.ApiGroupApp.BaseApi - { - groupRouter.POST("/search", baseApi.PageWebsiteSSL) - groupRouter.POST("", baseApi.CreateWebsiteSSL) - groupRouter.POST("/resolve", baseApi.GetDNSResolve) - groupRouter.POST("/del", baseApi.DeleteWebsiteSSL) - groupRouter.GET("/website/:websiteId", baseApi.GetWebsiteSSLByWebsiteId) - groupRouter.GET("/:id", baseApi.GetWebsiteSSLById) - groupRouter.POST("/update", baseApi.UpdateWebsiteSSL) - groupRouter.POST("/upload", baseApi.UploadWebsiteSSL) - groupRouter.POST("/obtain", baseApi.ApplyWebsiteSSL) - groupRouter.POST("/download", baseApi.DownloadWebsiteSSL) - } -} diff --git a/backend/server/init.go b/backend/server/init.go deleted file mode 100644 index b8ed30b47..000000000 --- a/backend/server/init.go +++ /dev/null @@ -1,6 +0,0 @@ -//go:build !xpack - -package server - -func InitOthers() { -} diff --git a/backend/server/server.go b/backend/server/server.go deleted file mode 100644 index 988949dd2..000000000 --- a/backend/server/server.go +++ /dev/null @@ -1,55 +0,0 @@ -package server - -import ( - "net" - "net/http" - - "github.com/1Panel-dev/1Panel/backend/cron" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/i18n" - "github.com/1Panel-dev/1Panel/backend/init/app" - "github.com/1Panel-dev/1Panel/backend/init/business" - "github.com/1Panel-dev/1Panel/backend/init/db" - "github.com/1Panel-dev/1Panel/backend/init/hook" - "github.com/1Panel-dev/1Panel/backend/init/log" - "github.com/1Panel-dev/1Panel/backend/init/migration" - "github.com/1Panel-dev/1Panel/backend/init/router" - "github.com/1Panel-dev/1Panel/backend/init/validator" - "github.com/1Panel-dev/1Panel/backend/init/viper" - - "github.com/gin-gonic/gin" -) - -func Start() { - viper.Init() - i18n.Init() - log.Init() - db.Init() - migration.Init() - app.Init() - validator.Init() - gin.SetMode("debug") - cron.Run() - InitOthers() - business.Init() - hook.Init() - - rootRouter := router.Routers() - - server := &http.Server{ - Addr: "0.0.0.0:9998", - Handler: rootRouter, - } - ln, err := net.Listen("tcp4", "0.0.0.0:9998") - if err != nil { - panic(err) - } - type tcpKeepAliveListener struct { - *net.TCPListener - } - - global.LOG.Info("listen at http://0.0.0.0:9998") - if err := server.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}); err != nil { - panic(err) - } -} diff --git a/backend/utils/cloud_storage/client/cos.go b/backend/utils/cloud_storage/client/cos.go deleted file mode 100644 index 0184c8ed4..000000000 --- a/backend/utils/cloud_storage/client/cos.go +++ /dev/null @@ -1,142 +0,0 @@ -package client - -import ( - "context" - "fmt" - "net/http" - "net/url" - "os" - - cosSDK "github.com/tencentyun/cos-go-sdk-v5" -) - -type cosClient struct { - scType string - client *cosSDK.Client - clientWithBucket *cosSDK.Client -} - -func NewCosClient(vars map[string]interface{}) (*cosClient, error) { - region := loadParamFromVars("region", vars) - accessKey := loadParamFromVars("accessKey", vars) - secretKey := loadParamFromVars("secretKey", vars) - bucket := loadParamFromVars("bucket", vars) - scType := loadParamFromVars("scType", vars) - if len(scType) == 0 { - scType = "Standard" - } - - u, _ := url.Parse(fmt.Sprintf("https://cos.%s.myqcloud.com", region)) - b := &cosSDK.BaseURL{BucketURL: u} - client := cosSDK.NewClient(b, &http.Client{ - Transport: &cosSDK.AuthorizationTransport{ - SecretID: accessKey, - SecretKey: secretKey, - }, - }) - - if len(bucket) != 0 { - u2, _ := url.Parse(fmt.Sprintf("https://%s.cos.%s.myqcloud.com", bucket, region)) - b2 := &cosSDK.BaseURL{BucketURL: u2} - clientWithBucket := cosSDK.NewClient(b2, &http.Client{ - Transport: &cosSDK.AuthorizationTransport{ - SecretID: accessKey, - SecretKey: secretKey, - }, - }) - return &cosClient{client: client, clientWithBucket: clientWithBucket, scType: scType}, nil - } - - return &cosClient{client: client, clientWithBucket: nil, scType: scType}, nil -} - -func (c cosClient) ListBuckets() ([]interface{}, error) { - buckets, _, err := c.client.Service.Get(context.Background()) - if err != nil { - return nil, err - } - var datas []interface{} - for _, bucket := range buckets.Buckets { - datas = append(datas, bucket.Name) - } - return datas, nil -} - -func (c cosClient) Exist(path string) (bool, error) { - exist, err := c.clientWithBucket.Object.IsExist(context.Background(), path) - if err != nil { - return false, err - } - return exist, nil -} - -func (c cosClient) Size(path string) (int64, error) { - data, _, err := c.clientWithBucket.Bucket.Get(context.Background(), &cosSDK.BucketGetOptions{Prefix: path}) - if err != nil { - return 0, err - } - if len(data.Contents) == 0 { - return 0, fmt.Errorf("no such file %s", path) - } - return data.Contents[0].Size, nil -} - -func (c cosClient) Delete(path string) (bool, error) { - if _, err := c.clientWithBucket.Object.Delete(context.Background(), path); err != nil { - return false, err - } - return true, nil -} - -func (c cosClient) Upload(src, target string) (bool, error) { - fileInfo, err := os.Stat(src) - if err != nil { - return false, err - } - if fileInfo.Size() > 5368709120 { - opt := &cosSDK.MultiUploadOptions{ - OptIni: &cosSDK.InitiateMultipartUploadOptions{ - ACLHeaderOptions: nil, - ObjectPutHeaderOptions: &cosSDK.ObjectPutHeaderOptions{ - XCosStorageClass: c.scType, - }, - }, - PartSize: 200, - } - if _, _, err := c.clientWithBucket.Object.MultiUpload( - context.Background(), target, src, opt, - ); err != nil { - return false, err - } - return true, nil - } - if _, err := c.clientWithBucket.Object.PutFromFile(context.Background(), target, src, &cosSDK.ObjectPutOptions{ - ACLHeaderOptions: nil, - ObjectPutHeaderOptions: &cosSDK.ObjectPutHeaderOptions{ - XCosStorageClass: c.scType, - }, - }); err != nil { - return false, err - } - return true, nil -} - -func (c cosClient) Download(src, target string) (bool, error) { - if _, err := c.clientWithBucket.Object.Download(context.Background(), src, target, &cosSDK.MultiDownloadOptions{}); err != nil { - return false, err - } - return true, nil -} - -func (c cosClient) ListObjects(prefix string) ([]string, error) { - datas, _, err := c.clientWithBucket.Bucket.Get(context.Background(), &cosSDK.BucketGetOptions{Prefix: prefix}) - if err != nil { - return nil, err - } - - var result []string - for _, item := range datas.Contents { - result = append(result, item.Key) - } - return result, nil -} diff --git a/backend/utils/cloud_storage/client/helper.go b/backend/utils/cloud_storage/client/helper.go deleted file mode 100644 index 37ebdb359..000000000 --- a/backend/utils/cloud_storage/client/helper.go +++ /dev/null @@ -1,18 +0,0 @@ -package client - -import ( - "fmt" - - "github.com/1Panel-dev/1Panel/backend/global" -) - -func loadParamFromVars(key string, vars map[string]interface{}) string { - if _, ok := vars[key]; !ok { - if key != "bucket" && key != "port" { - global.LOG.Errorf("load param %s from vars failed, err: not exist!", key) - } - return "" - } - - return fmt.Sprintf("%v", vars[key]) -} diff --git a/backend/utils/cloud_storage/client/kodo.go b/backend/utils/cloud_storage/client/kodo.go deleted file mode 100644 index d1fcf28eb..000000000 --- a/backend/utils/cloud_storage/client/kodo.go +++ /dev/null @@ -1,122 +0,0 @@ -package client - -import ( - "context" - "strconv" - "time" - - "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/qiniu/go-sdk/v7/auth" - "github.com/qiniu/go-sdk/v7/storage" -) - -type kodoClient struct { - bucket string - domain string - timeout string - auth *auth.Credentials - client *storage.BucketManager -} - -func NewKodoClient(vars map[string]interface{}) (*kodoClient, error) { - accessKey := loadParamFromVars("accessKey", vars) - secretKey := loadParamFromVars("secretKey", vars) - bucket := loadParamFromVars("bucket", vars) - domain := loadParamFromVars("domain", vars) - timeout := loadParamFromVars("timeout", vars) - if timeout == "" { - timeout = "1" - } - conn := auth.New(accessKey, secretKey) - cfg := storage.Config{ - UseHTTPS: false, - } - bucketManager := storage.NewBucketManager(conn, &cfg) - - return &kodoClient{client: bucketManager, auth: conn, bucket: bucket, domain: domain, timeout: timeout}, nil -} - -func (k kodoClient) ListBuckets() ([]interface{}, error) { - buckets, err := k.client.Buckets(true) - if err != nil { - return nil, err - } - var datas []interface{} - for _, bucket := range buckets { - datas = append(datas, bucket) - } - return datas, nil -} - -func (k kodoClient) Exist(path string) (bool, error) { - if _, err := k.client.Stat(k.bucket, path); err != nil { - return false, err - } - return true, nil -} - -func (k kodoClient) Size(path string) (int64, error) { - file, err := k.client.Stat(k.bucket, path) - if err != nil { - return 0, err - } - return file.Fsize, nil -} - -func (k kodoClient) Delete(path string) (bool, error) { - if err := k.client.Delete(k.bucket, path); err != nil { - return false, err - } - return true, nil -} - -func (k kodoClient) Upload(src, target string) (bool, error) { - - int64Value, _ := strconv.ParseInt(k.timeout, 10, 64) - unixTimestamp := int64Value * 3600 - - putPolicy := storage.PutPolicy{ - Scope: k.bucket, - Expires: uint64(unixTimestamp), - } - upToken := putPolicy.UploadToken(k.auth) - cfg := storage.Config{UseHTTPS: true, UseCdnDomains: false} - resumeUploader := storage.NewResumeUploaderV2(&cfg) - ret := storage.PutRet{} - putExtra := storage.RputV2Extra{} - if err := resumeUploader.PutFile(context.Background(), &ret, upToken, target, src, &putExtra); err != nil { - return false, err - } - return true, nil -} - -func (k kodoClient) Download(src, target string) (bool, error) { - deadline := time.Now().Add(time.Second * 3600).Unix() - privateAccessURL := storage.MakePrivateURL(k.auth, k.domain, src, deadline) - - fo := files.NewFileOp() - if err := fo.DownloadFile(privateAccessURL, target); err != nil { - return false, err - } - return true, nil -} - -func (k kodoClient) ListObjects(prefix string) ([]string, error) { - var result []string - marker := "" - for { - entries, _, nextMarker, hashNext, err := k.client.ListFiles(k.bucket, prefix, "", marker, 1000) - if err != nil { - return nil, err - } - for _, entry := range entries { - result = append(result, entry.Key) - } - if hashNext { - marker = nextMarker - } else { - break - } - } - return result, nil -} diff --git a/backend/utils/cloud_storage/client/local.go b/backend/utils/cloud_storage/client/local.go deleted file mode 100644 index 80f3e51e2..000000000 --- a/backend/utils/cloud_storage/client/local.go +++ /dev/null @@ -1,97 +0,0 @@ -package client - -import ( - "fmt" - "os" - "path" - "path/filepath" - - "github.com/1Panel-dev/1Panel/backend/utils/common" -) - -type localClient struct { - dir string -} - -func NewLocalClient(vars map[string]interface{}) (*localClient, error) { - dir := loadParamFromVars("dir", vars) - return &localClient{dir: dir}, nil -} - -func (c localClient) ListBuckets() ([]interface{}, error) { - return nil, nil -} - -func (c localClient) Exist(file string) (bool, error) { - _, err := os.Stat(path.Join(c.dir, file)) - return err == nil, err -} - -func (c localClient) Size(file string) (int64, error) { - fileInfo, err := os.Stat(path.Join(c.dir, file)) - if err != nil { - return 0, err - } - return fileInfo.Size(), nil -} - -func (c localClient) Delete(file string) (bool, error) { - if err := os.RemoveAll(path.Join(c.dir, file)); err != nil { - return false, err - } - return true, nil -} - -func (c localClient) Upload(src, target string) (bool, error) { - targetFilePath := path.Join(c.dir, target) - if _, err := os.Stat(path.Dir(targetFilePath)); err != nil { - if os.IsNotExist(err) { - if err = os.MkdirAll(path.Dir(targetFilePath), os.ModePerm); err != nil { - return false, err - } - } else { - return false, err - } - } - - if err := common.CopyFile(src, targetFilePath); err != nil { - return false, fmt.Errorf("cp file failed, err: %v", err) - } - return true, nil -} - -func (c localClient) Download(src, target string) (bool, error) { - localPath := path.Join(c.dir, src) - if _, err := os.Stat(path.Dir(target)); err != nil { - if os.IsNotExist(err) { - if err = os.MkdirAll(path.Dir(target), os.ModePerm); err != nil { - return false, err - } - } else { - return false, err - } - } - - if err := common.CopyFile(localPath, target); err != nil { - return false, fmt.Errorf("cp file failed, err: %v", err) - } - return true, nil -} - -func (c localClient) ListObjects(prefix string) ([]string, error) { - var files []string - itemPath := path.Join(c.dir, prefix) - if _, err := os.Stat(itemPath); err != nil { - return files, nil - } - if err := filepath.Walk(itemPath, func(path string, info os.FileInfo, err error) error { - if !info.IsDir() { - files = append(files, info.Name()) - } - return nil - }); err != nil { - return nil, err - } - - return files, nil -} diff --git a/backend/utils/cloud_storage/client/minio.go b/backend/utils/cloud_storage/client/minio.go deleted file mode 100644 index bedae3cfa..000000000 --- a/backend/utils/cloud_storage/client/minio.go +++ /dev/null @@ -1,149 +0,0 @@ -package client - -import ( - "context" - "crypto/tls" - "io" - "net/http" - "os" - "strings" - - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/minio/minio-go/v7" - "github.com/minio/minio-go/v7/pkg/credentials" -) - -type minIoClient struct { - bucket string - client *minio.Client -} - -func NewMinIoClient(vars map[string]interface{}) (*minIoClient, error) { - endpoint := loadParamFromVars("endpoint", vars) - accessKeyID := loadParamFromVars("accessKey", vars) - secretAccessKey := loadParamFromVars("secretKey", vars) - bucket := loadParamFromVars("bucket", vars) - ssl := strings.Split(endpoint, ":")[0] - if len(ssl) == 0 || (ssl != "https" && ssl != "http") { - return nil, constant.ErrInvalidParams - } - - secure := false - tlsConfig := &tls.Config{} - if ssl == "https" { - secure = true - tlsConfig.InsecureSkipVerify = true - } - var transport http.RoundTripper = &http.Transport{ - TLSClientConfig: tlsConfig, - } - client, err := minio.New(strings.ReplaceAll(endpoint, ssl+"://", ""), &minio.Options{ - Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""), - Secure: secure, - Transport: transport, - }) - if err != nil { - return nil, err - } - return &minIoClient{bucket: bucket, client: client}, nil -} - -func (m minIoClient) ListBuckets() ([]interface{}, error) { - buckets, err := m.client.ListBuckets(context.Background()) - if err != nil { - return nil, err - } - var result []interface{} - for _, bucket := range buckets { - result = append(result, bucket.Name) - } - return result, err -} - -func (m minIoClient) Exist(path string) (bool, error) { - if _, err := m.client.GetObject(context.Background(), m.bucket, path, minio.GetObjectOptions{}); err != nil { - return false, err - } - return true, nil -} - -func (m minIoClient) Size(path string) (int64, error) { - obj, err := m.client.GetObject(context.Background(), m.bucket, path, minio.GetObjectOptions{}) - if err != nil { - return 0, err - } - file, err := obj.Stat() - if err != nil { - return 0, err - } - return file.Size, nil -} - -func (m minIoClient) Delete(path string) (bool, error) { - object, err := m.client.GetObject(context.Background(), m.bucket, path, minio.GetObjectOptions{}) - if err != nil { - return false, err - } - info, err := object.Stat() - if err != nil { - return false, err - } - if err = m.client.RemoveObject(context.Background(), m.bucket, path, minio.RemoveObjectOptions{ - GovernanceBypass: true, - VersionID: info.VersionID, - }); err != nil { - return false, err - } - return true, nil -} - -func (m minIoClient) Upload(src, target string) (bool, error) { - file, err := os.Open(src) - if err != nil { - return false, err - } - defer file.Close() - - fileStat, err := file.Stat() - if err != nil { - return false, err - } - _, err = m.client.PutObject(context.Background(), m.bucket, target, file, fileStat.Size(), minio.PutObjectOptions{ContentType: "application/octet-stream"}) - if err != nil { - return false, err - } - return true, nil -} - -func (m minIoClient) Download(src, target string) (bool, error) { - object, err := m.client.GetObject(context.Background(), m.bucket, src, minio.GetObjectOptions{}) - if err != nil { - return false, err - } - defer object.Close() - localFile, err := os.Create(target) - if err != nil { - return false, err - } - defer localFile.Close() - if _, err = io.Copy(localFile, object); err != nil { - return false, err - } - return true, nil -} - -func (m minIoClient) ListObjects(prefix string) ([]string, error) { - opts := minio.ListObjectsOptions{ - Recursive: true, - Prefix: prefix, - } - - var result []string - for object := range m.client.ListObjects(context.Background(), m.bucket, opts) { - if object.Err != nil { - continue - } - result = append(result, object.Key) - } - return result, nil -} diff --git a/backend/utils/cloud_storage/client/onedrive.go b/backend/utils/cloud_storage/client/onedrive.go deleted file mode 100644 index 7ad0177d7..000000000 --- a/backend/utils/cloud_storage/client/onedrive.go +++ /dev/null @@ -1,405 +0,0 @@ -package client - -import ( - "bufio" - "bytes" - "context" - "crypto/tls" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "os" - "path" - "strconv" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/utils/files" - odsdk "github.com/goh-chunlin/go-onedrive/onedrive" - "golang.org/x/oauth2" -) - -type oneDriveClient struct { - client odsdk.Client -} - -func NewOneDriveClient(vars map[string]interface{}) (*oneDriveClient, error) { - token, err := RefreshToken("refresh_token", "accessToken", vars) - if err != nil { - return nil, err - } - isCN := loadParamFromVars("isCN", vars) - ctx := context.Background() - ts := oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: token}, - ) - tc := oauth2.NewClient(ctx, ts) - - client := odsdk.NewClient(tc) - if isCN == "true" { - client.BaseURL, _ = url.Parse("https://microsoftgraph.chinacloudapi.cn/v1.0/") - } - return &oneDriveClient{client: *client}, nil -} - -func (o oneDriveClient) ListBuckets() ([]interface{}, error) { - return nil, nil -} - -func (o oneDriveClient) Exist(path string) (bool, error) { - path = "/" + strings.TrimPrefix(path, "/") - fileID, err := o.loadIDByPath(path) - if err != nil { - return false, err - } - - return len(fileID) != 0, nil -} - -func (o oneDriveClient) Size(path string) (int64, error) { - path = "/" + strings.TrimPrefix(path, "/") - pathItem := "root:" + path - if path == "/" { - pathItem = "root" - } - req, err := o.client.NewRequest("GET", fmt.Sprintf("me/drive/%s", pathItem), nil) - if err != nil { - return 0, fmt.Errorf("new request for file id failed, err: %v", err) - } - var driveItem myDriverItem - if err := o.client.Do(context.Background(), req, false, &driveItem); err != nil { - return 0, fmt.Errorf("do request for file id failed, err: %v", err) - } - - return driveItem.Size, nil -} - -type myDriverItem struct { - Name string `json:"name"` - Id string `json:"id"` - Size int64 `json:"size"` -} - -func (o oneDriveClient) Delete(path string) (bool, error) { - path = "/" + strings.TrimPrefix(path, "/") - req, err := o.client.NewRequest("DELETE", fmt.Sprintf("me/drive/root:%s", path), nil) - if err != nil { - return false, fmt.Errorf("new request for delete file failed, err: %v \n", err) - } - if err := o.client.Do(context.Background(), req, false, nil); err != nil { - return false, fmt.Errorf("do request for delete file failed, err: %v \n", err) - } - - return true, nil -} - -func (o oneDriveClient) Upload(src, target string) (bool, error) { - target = "/" + strings.TrimPrefix(target, "/") - if _, err := o.loadIDByPath(path.Dir(target)); err != nil { - if !strings.Contains(err.Error(), "itemNotFound") { - return false, err - } - if err := o.createFolder(path.Dir(target)); err != nil { - return false, fmt.Errorf("create dir before upload failed, err: %v", err) - } - } - - ctx := context.Background() - folderID, err := o.loadIDByPath(path.Dir(target)) - if err != nil { - return false, err - } - fileInfo, err := os.Stat(src) - if err != nil { - return false, err - } - if fileInfo.IsDir() { - return false, errors.New("only file is allowed to be uploaded here") - } - var isOk bool - if fileInfo.Size() < 4*1024*1024 { - isOk, err = o.upSmall(src, folderID, fileInfo.Size()) - } else { - isOk, err = o.upBig(ctx, src, folderID, fileInfo.Size()) - } - return isOk, err -} - -func (o oneDriveClient) Download(src, target string) (bool, error) { - src = "/" + strings.TrimPrefix(src, "/") - req, err := o.client.NewRequest("GET", fmt.Sprintf("me/drive/root:%s", src), nil) - if err != nil { - return false, fmt.Errorf("new request for file id failed, err: %v", err) - } - var driveItem *odsdk.DriveItem - if err := o.client.Do(context.Background(), req, false, &driveItem); err != nil { - return false, fmt.Errorf("do request for file id failed, err: %v", err) - } - - resp, err := http.Get(driveItem.DownloadURL) - if err != nil { - return false, err - } - defer resp.Body.Close() - - out, err := os.Create(target) - if err != nil { - return false, err - } - defer out.Close() - buffer := make([]byte, 2*1024*1024) - - _, err = io.CopyBuffer(out, resp.Body, buffer) - if err != nil { - return false, err - } - - return true, nil -} - -func (o *oneDriveClient) ListObjects(prefix string) ([]string, error) { - prefix = "/" + strings.TrimPrefix(prefix, "/") - folderID, err := o.loadIDByPath(prefix) - if err != nil { - return nil, err - } - - req, err := o.client.NewRequest("GET", fmt.Sprintf("me/drive/items/%s/children", folderID), nil) - if err != nil { - return nil, fmt.Errorf("new request for list failed, err: %v", err) - } - var driveItems *odsdk.OneDriveDriveItemsResponse - if err := o.client.Do(context.Background(), req, false, &driveItems); err != nil { - return nil, fmt.Errorf("do request for list failed, err: %v", err) - } - - var itemList []string - for _, item := range driveItems.DriveItems { - itemList = append(itemList, item.Name) - } - return itemList, nil -} - -func (o *oneDriveClient) loadIDByPath(path string) (string, error) { - pathItem := "root:" + path - if path == "/" { - pathItem = "root" - } - req, err := o.client.NewRequest("GET", fmt.Sprintf("me/drive/%s", pathItem), nil) - if err != nil { - return "", fmt.Errorf("new request for file id failed, err: %v", err) - } - var driveItem *odsdk.DriveItem - if err := o.client.Do(context.Background(), req, false, &driveItem); err != nil { - return "", fmt.Errorf("do request for file id failed, err: %v", err) - } - return driveItem.Id, nil -} - -func RefreshToken(grantType string, tokenType string, varMap map[string]interface{}) (string, error) { - data := url.Values{} - isCN := loadParamFromVars("isCN", varMap) - data.Set("client_id", loadParamFromVars("client_id", varMap)) - data.Set("client_secret", loadParamFromVars("client_secret", varMap)) - if grantType == "refresh_token" { - data.Set("grant_type", "refresh_token") - data.Set("refresh_token", loadParamFromVars("refresh_token", varMap)) - } else { - data.Set("grant_type", "authorization_code") - data.Set("code", loadParamFromVars("code", varMap)) - } - data.Set("redirect_uri", loadParamFromVars("redirect_uri", varMap)) - client := &http.Client{} - url := "https://login.microsoftonline.com/common/oauth2/v2.0/token" - if isCN == "true" { - url = "https://login.chinacloudapi.cn/common/oauth2/v2.0/token" - } - req, err := http.NewRequest("POST", url, strings.NewReader(data.Encode())) - if err != nil { - return "", fmt.Errorf("new http post client for access token failed, err: %v", err) - } - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - resp, err := client.Do(req) - if err != nil { - return "", fmt.Errorf("request for access token failed, err: %v", err) - } - defer resp.Body.Close() - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("read data from response body failed, err: %v", err) - } - - tokenMap := map[string]interface{}{} - if err := json.Unmarshal(respBody, &tokenMap); err != nil { - return "", fmt.Errorf("unmarshal data from response body failed, err: %v", err) - } - if tokenType == "accessToken" { - accessToken, ok := tokenMap["access_token"].(string) - if !ok { - return "", errors.New("no such access token in response") - } - tokenMap = nil - return accessToken, nil - } - refreshToken, ok := tokenMap["refresh_token"].(string) - if !ok { - return "", errors.New("no such access token in response") - } - tokenMap = nil - return refreshToken, nil -} - -func (o *oneDriveClient) createFolder(parent string) error { - if _, err := o.loadIDByPath(path.Dir(parent)); err != nil { - if !strings.Contains(err.Error(), "itemNotFound") { - return err - } - _ = o.createFolder(path.Dir(parent)) - } - item2, err := o.loadIDByPath(path.Dir(parent)) - if err != nil { - return err - } - if _, err := o.client.DriveItems.CreateNewFolder(context.Background(), "", item2, path.Base(parent)); err != nil { - return err - } - return nil -} - -type NewUploadSessionCreationRequest struct { - ConflictBehavior string `json:"@microsoft.graph.conflictBehavior,omitempty"` -} -type NewUploadSessionCreationResponse struct { - UploadURL string `json:"uploadUrl"` - ExpirationDateTime string `json:"expirationDateTime"` -} -type UploadSessionUploadResponse struct { - ExpirationDateTime string `json:"expirationDateTime"` - NextExpectedRanges []string `json:"nextExpectedRanges"` - DriveItem -} -type DriveItem struct { - Name string `json:"name"` - Id string `json:"id"` - DownloadURL string `json:"@microsoft.graph.downloadUrl"` - Description string `json:"description"` - Size int64 `json:"size"` - WebURL string `json:"webUrl"` -} - -func (o *oneDriveClient) NewSessionFileUploadRequest(absoluteUrl string, grandOffset, grandTotalSize int64, byteReader *bytes.Reader) (*http.Request, error) { - apiUrl, err := o.client.BaseURL.Parse(absoluteUrl) - if err != nil { - return nil, err - } - absoluteUrl = apiUrl.String() - contentLength := byteReader.Size() - req, err := http.NewRequest("PUT", absoluteUrl, byteReader) - req.Header.Set("Content-Length", strconv.FormatInt(contentLength, 10)) - preliminaryLength := grandOffset - preliminaryRange := grandOffset + contentLength - 1 - if preliminaryRange >= grandTotalSize { - preliminaryRange = grandTotalSize - 1 - preliminaryLength = preliminaryRange - grandOffset + 1 - } - req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", preliminaryLength, preliminaryRange, grandTotalSize)) - - return req, err -} - -func (o *oneDriveClient) upSmall(srcPath, folderID string, fileSize int64) (bool, error) { - file, err := os.Open(srcPath) - if err != nil { - return false, err - } - defer file.Close() - - buffer := make([]byte, fileSize) - _, _ = file.Read(buffer) - fileReader := bytes.NewReader(buffer) - apiURL := fmt.Sprintf("me/drive/items/%s:/%s:/content?@microsoft.graph.conflictBehavior=rename", url.PathEscape(folderID), path.Base(srcPath)) - - mimeType := files.GetMimeType(srcPath) - req, err := o.client.NewFileUploadRequest(apiURL, mimeType, fileReader) - if err != nil { - return false, err - } - var response *DriveItem - if err := o.client.Do(context.Background(), req, false, &response); err != nil { - return false, fmt.Errorf("do request for list failed, err: %v", err) - } - return true, nil -} - -func (o *oneDriveClient) upBig(ctx context.Context, srcPath, folderID string, fileSize int64) (bool, error) { - file, err := os.Open(srcPath) - if err != nil { - return false, err - } - defer file.Close() - - apiURL := fmt.Sprintf("me/drive/items/%s:/%s:/createUploadSession", url.PathEscape(folderID), path.Base(srcPath)) - sessionCreationRequestInside := NewUploadSessionCreationRequest{ - ConflictBehavior: "rename", - } - - sessionCreationRequest := struct { - Item NewUploadSessionCreationRequest `json:"item"` - DeferCommit bool `json:"deferCommit"` - }{sessionCreationRequestInside, false} - - sessionCreationReq, err := o.client.NewRequest("POST", apiURL, sessionCreationRequest) - if err != nil { - return false, err - } - - var sessionCreationResp *NewUploadSessionCreationResponse - err = o.client.Do(ctx, sessionCreationReq, false, &sessionCreationResp) - if err != nil { - return false, fmt.Errorf("session creation failed %w", err) - } - - fileSessionUploadUrl := sessionCreationResp.UploadURL - - sizePerSplit := int64(5 * 1024 * 1024) - buffer := make([]byte, 5*1024*1024) - splitCount := fileSize / sizePerSplit - if fileSize%sizePerSplit != 0 { - splitCount += 1 - } - bfReader := bufio.NewReader(file) - httpClient := http.Client{ - Timeout: time.Minute * 10, - Transport: &http.Transport{ - Proxy: http.ProxyFromEnvironment, - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, - } - for splitNow := int64(0); splitNow < splitCount; splitNow++ { - length, err := bfReader.Read(buffer) - if err != nil { - return false, err - } - if int64(length) < sizePerSplit { - bufferLast := buffer[:length] - buffer = bufferLast - } - sessionFileUploadReq, err := o.NewSessionFileUploadRequest(fileSessionUploadUrl, splitNow*sizePerSplit, fileSize, bytes.NewReader(buffer)) - if err != nil { - return false, err - } - res, err := httpClient.Do(sessionFileUploadReq) - if err != nil { - return false, err - } - res.Body.Close() - if res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200 { - data, _ := io.ReadAll(res.Body) - return false, errors.New(string(data)) - } - } - return true, nil -} diff --git a/backend/utils/cloud_storage/client/oss.go b/backend/utils/cloud_storage/client/oss.go deleted file mode 100644 index e4fed15d7..000000000 --- a/backend/utils/cloud_storage/client/oss.go +++ /dev/null @@ -1,118 +0,0 @@ -package client - -import ( - "fmt" - - osssdk "github.com/aliyun/aliyun-oss-go-sdk/oss" -) - -type ossClient struct { - scType string - bucketStr string - client osssdk.Client -} - -func NewOssClient(vars map[string]interface{}) (*ossClient, error) { - endpoint := loadParamFromVars("endpoint", vars) - accessKey := loadParamFromVars("accessKey", vars) - secretKey := loadParamFromVars("secretKey", vars) - bucketStr := loadParamFromVars("bucket", vars) - scType := loadParamFromVars("scType", vars) - if len(scType) == 0 { - scType = "Standard" - } - client, err := osssdk.New(endpoint, accessKey, secretKey) - if err != nil { - return nil, err - } - - return &ossClient{scType: scType, bucketStr: bucketStr, client: *client}, nil -} - -func (o ossClient) ListBuckets() ([]interface{}, error) { - response, err := o.client.ListBuckets() - if err != nil { - return nil, err - } - var result []interface{} - for _, bucket := range response.Buckets { - result = append(result, bucket.Name) - } - return result, err -} - -func (o ossClient) Exist(path string) (bool, error) { - bucket, err := o.client.Bucket(o.bucketStr) - if err != nil { - return false, err - } - return bucket.IsObjectExist(path) -} - -func (o ossClient) Size(path string) (int64, error) { - bucket, err := o.client.Bucket(o.bucketStr) - if err != nil { - return 0, err - } - lor, err := bucket.ListObjectsV2(osssdk.Prefix(path)) - if err != nil { - return 0, err - } - if len(lor.Objects) == 0 { - return 0, fmt.Errorf("no such file %s", path) - } - return lor.Objects[0].Size, nil -} - -func (o ossClient) Delete(path string) (bool, error) { - bucket, err := o.client.Bucket(o.bucketStr) - if err != nil { - return false, err - } - if err := bucket.DeleteObject(path); err != nil { - return false, err - } - return true, nil -} - -func (o ossClient) Upload(src, target string) (bool, error) { - bucket, err := o.client.Bucket(o.bucketStr) - if err != nil { - return false, err - } - if err := bucket.UploadFile(target, src, - 200*1024*1024, - osssdk.Routines(5), - osssdk.Checkpoint(true, ""), - osssdk.ObjectStorageClass(osssdk.StorageClassType(o.scType))); err != nil { - return false, err - } - return true, nil -} - -func (o ossClient) Download(src, target string) (bool, error) { - bucket, err := o.client.Bucket(o.bucketStr) - if err != nil { - return false, err - } - if err := bucket.DownloadFile(src, target, 200*1024*1024, osssdk.Routines(5), osssdk.Checkpoint(true, "")); err != nil { - return false, err - } - return true, nil -} - -func (o *ossClient) ListObjects(prefix string) ([]string, error) { - bucket, err := o.client.Bucket(o.bucketStr) - if err != nil { - return nil, err - } - lor, err := bucket.ListObjectsV2(osssdk.Prefix(prefix)) - if err != nil { - return nil, err - } - var result []string - for _, obj := range lor.Objects { - result = append(result, obj.Key) - } - return result, nil -} diff --git a/backend/utils/cloud_storage/client/s3.go b/backend/utils/cloud_storage/client/s3.go deleted file mode 100644 index 9fc7ebadd..000000000 --- a/backend/utils/cloud_storage/client/s3.go +++ /dev/null @@ -1,163 +0,0 @@ -package client - -import ( - "os" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/s3" - "github.com/aws/aws-sdk-go/service/s3/s3manager" -) - -type s3Client struct { - scType string - bucket string - Sess session.Session -} - -func NewS3Client(vars map[string]interface{}) (*s3Client, error) { - accessKey := loadParamFromVars("accessKey", vars) - secretKey := loadParamFromVars("secretKey", vars) - endpoint := loadParamFromVars("endpoint", vars) - region := loadParamFromVars("region", vars) - bucket := loadParamFromVars("bucket", vars) - scType := loadParamFromVars("scType", vars) - if len(scType) == 0 { - scType = "Standard" - } - sess, err := session.NewSession(&aws.Config{ - Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""), - Endpoint: aws.String(endpoint), - Region: aws.String(region), - DisableSSL: aws.Bool(true), - S3ForcePathStyle: aws.Bool(false), - }) - if err != nil { - return nil, err - } - return &s3Client{scType: scType, bucket: bucket, Sess: *sess}, nil -} - -func (s s3Client) ListBuckets() ([]interface{}, error) { - var result []interface{} - svc := s3.New(&s.Sess) - res, err := svc.ListBuckets(nil) - if err != nil { - return nil, err - } - for _, b := range res.Buckets { - result = append(result, b.Name) - } - return result, nil -} - -func (s s3Client) Exist(path string) (bool, error) { - svc := s3.New(&s.Sess) - if _, err := svc.HeadObject(&s3.HeadObjectInput{ - Bucket: &s.bucket, - Key: &path, - }); err != nil { - if aerr, ok := err.(awserr.RequestFailure); ok { - if aerr.StatusCode() == 404 { - return false, nil - } - } else { - return false, aerr - } - } - return true, nil -} - -func (s *s3Client) Size(path string) (int64, error) { - svc := s3.New(&s.Sess) - file, err := svc.GetObject(&s3.GetObjectInput{ - Bucket: &s.bucket, - Key: &path, - }) - if err != nil { - return 0, err - } - return *file.ContentLength, nil -} - -func (s s3Client) Delete(path string) (bool, error) { - svc := s3.New(&s.Sess) - if _, err := svc.DeleteObject(&s3.DeleteObjectInput{Bucket: aws.String(s.bucket), Key: aws.String(path)}); err != nil { - return false, err - } - if err := svc.WaitUntilObjectNotExists(&s3.HeadObjectInput{ - Bucket: aws.String(s.bucket), - Key: aws.String(path), - }); err != nil { - return false, err - } - return true, nil -} - -func (s s3Client) Upload(src, target string) (bool, error) { - fileInfo, err := os.Stat(src) - if err != nil { - return false, err - } - file, err := os.Open(src) - if err != nil { - return false, err - } - defer file.Close() - - uploader := s3manager.NewUploader(&s.Sess) - if fileInfo.Size() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize { - uploader.PartSize = fileInfo.Size() / (s3manager.MaxUploadParts - 1) - } - if _, err := uploader.Upload(&s3manager.UploadInput{ - Bucket: aws.String(s.bucket), - Key: aws.String(target), - Body: file, - StorageClass: &s.scType, - }); err != nil { - return false, err - } - return true, nil -} - -func (s s3Client) Download(src, target string) (bool, error) { - if _, err := os.Stat(target); err != nil { - if os.IsNotExist(err) { - os.Remove(target) - } else { - return false, err - } - } - file, err := os.Create(target) - if err != nil { - return false, err - } - defer file.Close() - downloader := s3manager.NewDownloader(&s.Sess) - if _, err = downloader.Download(file, &s3.GetObjectInput{ - Bucket: aws.String(s.bucket), - Key: aws.String(src), - }); err != nil { - os.Remove(target) - return false, err - } - return true, nil -} - -func (s *s3Client) ListObjects(prefix string) ([]string, error) { - svc := s3.New(&s.Sess) - var result []string - outputs, err := svc.ListObjects(&s3.ListObjectsInput{ - Bucket: &s.bucket, - Prefix: &prefix, - }) - if err != nil { - return result, err - } - for _, item := range outputs.Contents { - result = append(result, *item.Key) - } - return result, nil -} diff --git a/backend/utils/cloud_storage/client/sftp.go b/backend/utils/cloud_storage/client/sftp.go deleted file mode 100644 index d544315bf..000000000 --- a/backend/utils/cloud_storage/client/sftp.go +++ /dev/null @@ -1,206 +0,0 @@ -package client - -import ( - "fmt" - "io" - "net" - "os" - "path" - "time" - - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/pkg/sftp" - "golang.org/x/crypto/ssh" -) - -type sftpClient struct { - bucket string - connInfo string - config *ssh.ClientConfig -} - -func NewSftpClient(vars map[string]interface{}) (*sftpClient, error) { - address := loadParamFromVars("address", vars) - port := loadParamFromVars("port", vars) - if len(port) == 0 { - global.LOG.Errorf("load param port from vars failed, err: not exist!") - } - password := loadParamFromVars("password", vars) - username := loadParamFromVars("username", vars) - bucket := loadParamFromVars("bucket", vars) - - auth := []ssh.AuthMethod{ssh.Password(password)} - clientConfig := &ssh.ClientConfig{ - User: username, - Auth: auth, - Timeout: 30 * time.Second, - HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { - return nil - }, - } - if _, err := ssh.Dial("tcp", fmt.Sprintf("%s:%s", address, port), clientConfig); err != nil { - return nil, err - } - - return &sftpClient{bucket: bucket, connInfo: fmt.Sprintf("%s:%s", address, port), config: clientConfig}, nil -} - -func (s sftpClient) Upload(src, target string) (bool, error) { - sshClient, err := ssh.Dial("tcp", s.connInfo, s.config) - if err != nil { - return false, err - } - defer sshClient.Close() - client, err := sftp.NewClient(sshClient) - if err != nil { - return false, err - } - defer client.Close() - - srcFile, err := os.Open(src) - if err != nil { - return false, err - } - defer srcFile.Close() - - targetFilePath := path.Join(s.bucket, target) - targetDir, _ := path.Split(targetFilePath) - if _, err = client.Stat(targetDir); err != nil { - if os.IsNotExist(err) { - if err = client.MkdirAll(targetDir); err != nil { - return false, err - } - } else { - return false, err - } - } - dstFile, err := client.Create(path.Join(s.bucket, target)) - if err != nil { - return false, err - } - defer dstFile.Close() - - if _, err := io.Copy(dstFile, srcFile); err != nil { - return false, err - } - return true, nil -} - -func (s sftpClient) ListBuckets() ([]interface{}, error) { - var result []interface{} - return result, nil -} - -func (s sftpClient) Download(src, target string) (bool, error) { - sshClient, err := ssh.Dial("tcp", s.connInfo, s.config) - if err != nil { - return false, err - } - client, err := sftp.NewClient(sshClient) - if err != nil { - return false, err - } - defer client.Close() - defer sshClient.Close() - - srcFile, err := client.Open(s.bucket + "/" + src) - if err != nil { - return false, err - } - defer srcFile.Close() - - dstFile, err := os.Create(target) - if err != nil { - return false, err - } - defer dstFile.Close() - - if _, err = io.Copy(dstFile, srcFile); err != nil { - return false, err - } - return true, err -} - -func (s sftpClient) Exist(filePath string) (bool, error) { - sshClient, err := ssh.Dial("tcp", s.connInfo, s.config) - if err != nil { - return false, err - } - client, err := sftp.NewClient(sshClient) - if err != nil { - return false, err - } - defer client.Close() - defer sshClient.Close() - - srcFile, err := client.Open(path.Join(s.bucket, filePath)) - if err != nil { - if os.IsNotExist(err) { - return false, nil - } else { - return false, err - } - } - defer srcFile.Close() - return true, err -} - -func (s sftpClient) Size(filePath string) (int64, error) { - sshClient, err := ssh.Dial("tcp", s.connInfo, s.config) - if err != nil { - return 0, err - } - client, err := sftp.NewClient(sshClient) - if err != nil { - return 0, err - } - defer client.Close() - defer sshClient.Close() - - files, err := client.Stat(path.Join(s.bucket, filePath)) - if err != nil { - return 0, err - } - return files.Size(), nil -} - -func (s sftpClient) Delete(filePath string) (bool, error) { - sshClient, err := ssh.Dial("tcp", s.connInfo, s.config) - if err != nil { - return false, err - } - client, err := sftp.NewClient(sshClient) - if err != nil { - return false, err - } - defer client.Close() - defer sshClient.Close() - - if err := client.Remove(path.Join(s.bucket, filePath)); err != nil { - return false, err - } - return true, nil -} - -func (s sftpClient) ListObjects(prefix string) ([]string, error) { - sshClient, err := ssh.Dial("tcp", s.connInfo, s.config) - if err != nil { - return nil, err - } - client, err := sftp.NewClient(sshClient) - if err != nil { - return nil, err - } - defer client.Close() - defer sshClient.Close() - - files, err := client.ReadDir(path.Join(s.bucket, prefix)) - if err != nil { - return nil, err - } - var result []string - for _, file := range files { - result = append(result, file.Name()) - } - return result, nil -} diff --git a/backend/utils/cloud_storage/client/webdav.go b/backend/utils/cloud_storage/client/webdav.go deleted file mode 100644 index c1013357c..000000000 --- a/backend/utils/cloud_storage/client/webdav.go +++ /dev/null @@ -1,121 +0,0 @@ -package client - -import ( - "crypto/tls" - "fmt" - "io" - "net/http" - "os" - "path" - "strings" - - "github.com/studio-b12/gowebdav" -) - -type webDAVClient struct { - Bucket string - client *gowebdav.Client -} - -func NewWebDAVClient(vars map[string]interface{}) (*webDAVClient, error) { - address := loadParamFromVars("address", vars) - port := loadParamFromVars("port", vars) - password := loadParamFromVars("password", vars) - username := loadParamFromVars("username", vars) - bucket := loadParamFromVars("bucket", vars) - - url := fmt.Sprintf("%s:%s", address, port) - if len(port) == 0 { - url = address - } - client := gowebdav.NewClient(url, username, password) - tlsConfig := &tls.Config{} - if strings.HasPrefix(address, "https") { - tlsConfig.InsecureSkipVerify = true - } - var transport http.RoundTripper = &http.Transport{ - TLSClientConfig: tlsConfig, - } - client.SetTransport(transport) - if err := client.Connect(); err != nil { - return nil, err - } - return &webDAVClient{Bucket: bucket, client: client}, nil -} - -func (s webDAVClient) Upload(src, target string) (bool, error) { - targetFilePath := path.Join(s.Bucket, target) - srcFile, err := os.Open(src) - if err != nil { - return false, err - } - defer srcFile.Close() - - if err := s.client.WriteStream(targetFilePath, srcFile, 0644); err != nil { - return false, err - } - return true, nil -} - -func (s webDAVClient) ListBuckets() ([]interface{}, error) { - var result []interface{} - return result, nil -} - -func (s webDAVClient) Download(src, target string) (bool, error) { - srcPath := path.Join(s.Bucket, src) - info, err := s.client.Stat(srcPath) - if err != nil { - return false, err - } - targetStat, err := os.Stat(target) - if err == nil { - if info.Size() == targetStat.Size() { - return true, nil - } - } - file, err := os.Create(target) - if err != nil { - return false, err - } - defer file.Close() - reader, _ := s.client.ReadStream(srcPath) - if _, err := io.Copy(file, reader); err != nil { - return false, err - } - return true, err -} - -func (s webDAVClient) Exist(pathItem string) (bool, error) { - if _, err := s.client.Stat(path.Join(s.Bucket, pathItem)); err != nil { - return false, err - } - return true, nil -} - -func (s webDAVClient) Size(pathItem string) (int64, error) { - file, err := s.client.Stat(path.Join(s.Bucket, pathItem)) - if err != nil { - return 0, err - } - return file.Size(), nil -} - -func (s webDAVClient) Delete(pathItem string) (bool, error) { - if err := s.client.Remove(path.Join(s.Bucket, pathItem)); err != nil { - return false, err - } - return true, nil -} - -func (s webDAVClient) ListObjects(prefix string) ([]string, error) { - files, err := s.client.ReadDir(path.Join(s.Bucket, prefix)) - if err != nil { - return nil, err - } - var result []string - for _, file := range files { - result = append(result, file.Name()) - } - return result, nil -} diff --git a/backend/utils/cloud_storage/cloud_storage_client.go b/backend/utils/cloud_storage/cloud_storage_client.go deleted file mode 100644 index 44412e7ba..000000000 --- a/backend/utils/cloud_storage/cloud_storage_client.go +++ /dev/null @@ -1,42 +0,0 @@ -package cloud_storage - -import ( - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/cloud_storage/client" -) - -type CloudStorageClient interface { - ListBuckets() ([]interface{}, error) - ListObjects(prefix string) ([]string, error) - Exist(path string) (bool, error) - Delete(path string) (bool, error) - Upload(src, target string) (bool, error) - Download(src, target string) (bool, error) - - Size(path string) (int64, error) -} - -func NewCloudStorageClient(backupType string, vars map[string]interface{}) (CloudStorageClient, error) { - switch backupType { - case constant.Local: - return client.NewLocalClient(vars) - case constant.S3: - return client.NewS3Client(vars) - case constant.OSS: - return client.NewOssClient(vars) - case constant.Sftp: - return client.NewSftpClient(vars) - case constant.WebDAV: - return client.NewWebDAVClient(vars) - case constant.MinIo: - return client.NewMinIoClient(vars) - case constant.Cos: - return client.NewCosClient(vars) - case constant.Kodo: - return client.NewKodoClient(vars) - case constant.OneDrive: - return client.NewOneDriveClient(vars) - default: - return nil, constant.ErrNotSupportType - } -} diff --git a/backend/utils/cmd/cmd.go b/backend/utils/cmd/cmd.go deleted file mode 100644 index 46b20fc1f..000000000 --- a/backend/utils/cmd/cmd.go +++ /dev/null @@ -1,229 +0,0 @@ -package cmd - -import ( - "bytes" - "context" - "errors" - "fmt" - "log" - "os" - "os/exec" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" -) - -func Exec(cmdStr string) (string, error) { - return ExecWithTimeOut(cmdStr, 20*time.Second) -} - -func handleErr(stdout, stderr bytes.Buffer, err error) (string, error) { - errMsg := "" - if len(stderr.String()) != 0 { - errMsg = fmt.Sprintf("stderr: %s", stderr.String()) - } - if len(stdout.String()) != 0 { - if len(errMsg) != 0 { - errMsg = fmt.Sprintf("%s; stdout: %s", errMsg, stdout.String()) - } else { - errMsg = fmt.Sprintf("stdout: %s", stdout.String()) - } - } - return errMsg, err -} - -func ExecWithTimeOut(cmdStr string, timeout time.Duration) (string, error) { - cmd := exec.Command("bash", "-c", cmdStr) - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - if err := cmd.Start(); err != nil { - return "", err - } - done := make(chan error, 1) - go func() { - done <- cmd.Wait() - }() - after := time.After(timeout) - select { - case <-after: - _ = cmd.Process.Kill() - return "", buserr.New(constant.ErrCmdTimeout) - case err := <-done: - if err != nil { - return handleErr(stdout, stderr, err) - } - } - - return stdout.String(), nil -} - -func ExecContainerScript(containerName, cmdStr string, timeout time.Duration) error { - cmdStr = fmt.Sprintf("docker exec -i %s bash -c '%s'", containerName, cmdStr) - out, err := ExecWithTimeOut(cmdStr, timeout) - if err != nil { - if out != "" { - return fmt.Errorf("%s; err: %v", out, err) - } - return err - } - return nil -} - -func ExecCronjobWithTimeOut(cmdStr, workdir, outPath string, timeout time.Duration) error { - file, err := os.OpenFile(outPath, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - return err - } - defer file.Close() - - cmd := exec.Command("bash", "-c", cmdStr) - cmd.Dir = workdir - cmd.Stdout = file - cmd.Stderr = file - if err := cmd.Start(); err != nil { - return err - } - done := make(chan error, 1) - go func() { - done <- cmd.Wait() - }() - after := time.After(timeout) - select { - case <-after: - _ = cmd.Process.Kill() - return buserr.New(constant.ErrCmdTimeout) - case err := <-done: - if err != nil { - return err - } - } - return nil -} - -func Execf(cmdStr string, a ...interface{}) (string, error) { - cmd := exec.Command("bash", "-c", fmt.Sprintf(cmdStr, a...)) - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - err := cmd.Run() - if err != nil { - return handleErr(stdout, stderr, err) - } - return stdout.String(), nil -} - -func ExecWithCheck(name string, a ...string) (string, error) { - cmd := exec.Command(name, a...) - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - err := cmd.Run() - if err != nil { - return handleErr(stdout, stderr, err) - } - return stdout.String(), nil -} - -func ExecScript(scriptPath, workDir string) (string, error) { - cmd := exec.Command("bash", scriptPath) - var stdout, stderr bytes.Buffer - cmd.Dir = workDir - cmd.Stdout = &stdout - cmd.Stderr = &stderr - if err := cmd.Start(); err != nil { - return "", err - } - done := make(chan error, 1) - go func() { - done <- cmd.Wait() - }() - after := time.After(10 * time.Minute) - select { - case <-after: - _ = cmd.Process.Kill() - return "", buserr.New(constant.ErrCmdTimeout) - case err := <-done: - if err != nil { - return handleErr(stdout, stderr, err) - } - } - - return stdout.String(), nil -} - -func ExecCmd(cmdStr string) error { - cmd := exec.Command("bash", "-c", cmdStr) - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("error : %v, output: %s", err, output) - } - return nil -} - -func ExecCmdWithDir(cmdStr, workDir string) error { - cmd := exec.Command("bash", "-c", cmdStr) - cmd.Dir = workDir - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("error : %v, output: %s", err, output) - } - return nil -} - -func CheckIllegal(args ...string) bool { - if args == nil { - return false - } - for _, arg := range args { - if strings.Contains(arg, "&") || strings.Contains(arg, "|") || strings.Contains(arg, ";") || - strings.Contains(arg, "$") || strings.Contains(arg, "'") || strings.Contains(arg, "`") || - strings.Contains(arg, "(") || strings.Contains(arg, ")") || strings.Contains(arg, "\"") || - strings.Contains(arg, "\n") || strings.Contains(arg, "\r") || strings.Contains(arg, ">") || strings.Contains(arg, "<") { - return true - } - } - return false -} - -func HasNoPasswordSudo() bool { - cmd2 := exec.Command("sudo", "-n", "ls") - err2 := cmd2.Run() - return err2 == nil -} - -func SudoHandleCmd() string { - cmd := exec.Command("sudo", "-n", "ls") - if err := cmd.Run(); err == nil { - return "sudo " - } - return "" -} - -func Which(name string) bool { - stdout, err := Execf("which %s", name) - if err != nil || (len(strings.ReplaceAll(stdout, "\n", "")) == 0 && strings.HasPrefix(stdout, "/")) { - return false - } - return true -} - -func ExecShellWithTimeOut(cmdStr, workdir string, logger *log.Logger, timeout time.Duration) error { - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - cmd := exec.CommandContext(ctx, "bash", "-c", cmdStr) - cmd.Dir = workdir - cmd.Stdout = logger.Writer() - cmd.Stderr = logger.Writer() - if err := cmd.Start(); err != nil { - return err - } - err := cmd.Wait() - if errors.Is(ctx.Err(), context.DeadlineExceeded) { - return buserr.New(constant.ErrCmdTimeout) - } - return err -} diff --git a/backend/utils/common/common.go b/backend/utils/common/common.go deleted file mode 100644 index d0def7382..000000000 --- a/backend/utils/common/common.go +++ /dev/null @@ -1,327 +0,0 @@ -package common - -import ( - "crypto/rand" - "fmt" - "io" - mathRand "math/rand" - "net" - "os" - "path" - "reflect" - "regexp" - "sort" - "strconv" - "strings" - "time" - "unicode" - - "golang.org/x/net/idna" - - "github.com/1Panel-dev/1Panel/backend/utils/cmd" -) - -func CompareVersion(version1, version2 string) bool { - v1s := extractNumbers(version1) - v2s := extractNumbers(version2) - - maxLen := max(len(v1s), len(v2s)) - v1s = append(v1s, make([]string, maxLen-len(v1s))...) - v2s = append(v2s, make([]string, maxLen-len(v2s))...) - - for i := 0; i < maxLen; i++ { - v1, err1 := strconv.Atoi(v1s[i]) - v2, err2 := strconv.Atoi(v2s[i]) - if err1 != nil { - v1 = 0 - } - if err2 != nil { - v2 = 0 - } - if v1 != v2 { - return v1 > v2 - } - } - return false -} - -func ComparePanelVersion(version1, version2 string) bool { - if version1 == version2 { - return false - } - version1s := SplitStr(version1, ".", "-") - version2s := SplitStr(version2, ".", "-") - - if len(version2s) > len(version1s) { - for i := 0; i < len(version2s)-len(version1s); i++ { - version1s = append(version1s, "0") - } - } - if len(version1s) > len(version2s) { - for i := 0; i < len(version1s)-len(version2s); i++ { - version2s = append(version2s, "0") - } - } - - n := min(len(version1s), len(version2s)) - for i := 0; i < n; i++ { - if version1s[i] == version2s[i] { - continue - } else { - v1, err1 := strconv.Atoi(version1s[i]) - if err1 != nil { - return version1s[i] > version2s[i] - } - v2, err2 := strconv.Atoi(version2s[i]) - if err2 != nil { - return version1s[i] > version2s[i] - } - return v1 > v2 - } - } - return true -} - -func extractNumbers(version string) []string { - var numbers []string - start := -1 - for i, r := range version { - if isDigit(r) { - if start == -1 { - start = i - } - } else { - if start != -1 { - numbers = append(numbers, version[start:i]) - start = -1 - } - } - } - if start != -1 { - numbers = append(numbers, version[start:]) - } - return numbers -} - -func isDigit(r rune) bool { - return r >= '0' && r <= '9' -} - -func max(x, y int) int { - if x > y { - return x - } - return y -} - -func GetSortedVersions(versions []string) []string { - sort.Slice(versions, func(i, j int) bool { - return CompareVersion(versions[i], versions[j]) - }) - return versions -} - -func CopyFile(src, dst string) error { - source, err := os.Open(src) - if err != nil { - return err - } - defer source.Close() - - if path.Base(src) != path.Base(dst) { - dst = path.Join(dst, path.Base(src)) - } - if _, err := os.Stat(path.Dir(dst)); err != nil { - if os.IsNotExist(err) { - _ = os.MkdirAll(path.Dir(dst), os.ModePerm) - } - } - target, err := os.OpenFile(dst+"_temp", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) - if err != nil { - return err - } - defer target.Close() - - if _, err = io.Copy(target, source); err != nil { - return err - } - if err = os.Rename(dst+"_temp", dst); err != nil { - return err - } - return nil -} - -func IsCrossVersion(version1, version2 string) bool { - version1s := strings.Split(version1, ".") - version2s := strings.Split(version2, ".") - v1num, _ := strconv.Atoi(version1s[0]) - v2num, _ := strconv.Atoi(version2s[0]) - return v2num > v1num -} - -func GetUuid() string { - b := make([]byte, 16) - _, _ = io.ReadFull(rand.Reader, b) - b[6] = (b[6] & 0x0f) | 0x40 - b[8] = (b[8] & 0x3f) | 0x80 - return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) -} - -var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890") - -func RandStr(n int) string { - b := make([]rune, n) - for i := range b { - b[i] = letters[mathRand.Intn(len(letters))] - } - return string(b) -} - -func RandStrAndNum(n int) string { - source := mathRand.NewSource(time.Now().UnixNano()) - randGen := mathRand.New(source) - const charset = "abcdefghijklmnopqrstuvwxyz0123456789" - b := make([]byte, n) - for i := range b { - b[i] = charset[randGen.Intn(len(charset)-1)] - } - return (string(b)) -} - -func ScanPort(port int) bool { - ln, err := net.Listen("tcp", ":"+strconv.Itoa(port)) - if err != nil { - return true - } - defer ln.Close() - return false -} - -func ScanUDPPort(port int) bool { - ln, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: port}) - if err != nil { - return true - } - defer ln.Close() - return false -} - -func ScanPortWithProto(port int, proto string) bool { - if proto == "udp" { - return ScanUDPPort(port) - } - return ScanPort(port) -} - -func IsNum(s string) bool { - _, err := strconv.ParseFloat(s, 64) - return err == nil -} - -func RemoveRepeatElement(a interface{}) (ret []interface{}) { - va := reflect.ValueOf(a) - for i := 0; i < va.Len(); i++ { - if i > 0 && reflect.DeepEqual(va.Index(i-1).Interface(), va.Index(i).Interface()) { - continue - } - ret = append(ret, va.Index(i).Interface()) - } - return ret -} - -func LoadSizeUnit(value float64) string { - val := int64(value) - if val%1024 != 0 { - return fmt.Sprintf("%v", val) - } - if val > 1048576 { - return fmt.Sprintf("%vM", val/1048576) - } - if val > 1024 { - return fmt.Sprintf("%vK", val/1024) - } - return fmt.Sprintf("%v", val) -} - -func LoadSizeUnit2F(value float64) string { - if value > 1073741824 { - return fmt.Sprintf("%.2fG", value/1073741824) - } - if value > 1048576 { - return fmt.Sprintf("%.2fM", value/1048576) - } - if value > 1024 { - return fmt.Sprintf("%.2fK", value/1024) - } - return fmt.Sprintf("%.2f", value) -} - -func LoadTimeZone() string { - loc := time.Now().Location() - if _, err := time.LoadLocation(loc.String()); err != nil { - return "Asia/Shanghai" - } - return loc.String() -} -func LoadTimeZoneByCmd() string { - loc := time.Now().Location().String() - if _, err := time.LoadLocation(loc); err != nil { - loc = "Asia/Shanghai" - } - std, err := cmd.Exec("timedatectl | grep 'Time zone'") - if err != nil { - return loc - } - fields := strings.Fields(string(std)) - if len(fields) != 5 { - return loc - } - if _, err := time.LoadLocation(fields[2]); err != nil { - return loc - } - return fields[2] -} - -func IsValidDomain(domain string) bool { - pattern := `^([\w\p{Han}\-\*]{1,100}\.){1,10}([\w\p{Han}\-]{1,24}|[\w\p{Han}\-]{1,24}\.[\w\p{Han}\-]{1,24})(:\d{1,5})?$` - match, err := regexp.MatchString(pattern, domain) - if err != nil { - return false - } - return match -} - -func ContainsChinese(text string) bool { - for _, char := range text { - if unicode.Is(unicode.Han, char) { - return true - } - } - return false -} - -func PunycodeEncode(text string) (string, error) { - encoder := idna.New() - ascii, err := encoder.ToASCII(text) - if err != nil { - return "", err - } - return ascii, nil -} - -func SplitStr(str string, spi ...string) []string { - lists := []string{str} - var results []string - for _, s := range spi { - results = []string{} - for _, list := range lists { - results = append(results, strings.Split(list, s)...) - } - lists = results - } - return results -} - -func IsValidIP(ip string) bool { - return net.ParseIP(ip) != nil -} diff --git a/backend/utils/compose/compose.go b/backend/utils/compose/compose.go deleted file mode 100644 index 08ade3bba..000000000 --- a/backend/utils/compose/compose.go +++ /dev/null @@ -1,40 +0,0 @@ -package compose - -import ( - "github.com/1Panel-dev/1Panel/backend/utils/cmd" -) - -func Pull(filePath string) (string, error) { - stdout, err := cmd.Execf("docker-compose -f %s pull", filePath) - return stdout, err -} - -func Up(filePath string) (string, error) { - stdout, err := cmd.Execf("docker-compose -f %s up -d", filePath) - return stdout, err -} - -func Down(filePath string) (string, error) { - stdout, err := cmd.Execf("docker-compose -f %s down --remove-orphans", filePath) - return stdout, err -} - -func Start(filePath string) (string, error) { - stdout, err := cmd.Execf("docker-compose -f %s start", filePath) - return stdout, err -} - -func Stop(filePath string) (string, error) { - stdout, err := cmd.Execf("docker-compose -f %s stop", filePath) - return stdout, err -} - -func Restart(filePath string) (string, error) { - stdout, err := cmd.Execf("docker-compose -f %s restart", filePath) - return stdout, err -} - -func Operate(filePath, operation string) (string, error) { - stdout, err := cmd.Execf("docker-compose -f %s %s", filePath, operation) - return stdout, err -} diff --git a/backend/utils/copier/copier.go b/backend/utils/copier/copier.go deleted file mode 100644 index bfc684ce9..000000000 --- a/backend/utils/copier/copier.go +++ /dev/null @@ -1,18 +0,0 @@ -package copier - -import ( - "encoding/json" - - "github.com/pkg/errors" -) - -func Copy(to, from interface{}) error { - b, err := json.Marshal(from) - if err != nil { - return errors.Wrap(err, "marshal from data err") - } - if err = json.Unmarshal(b, to); err != nil { - return errors.Wrap(err, "unmarshal to data err") - } - return nil -} diff --git a/backend/utils/docker/compose.go b/backend/utils/docker/compose.go deleted file mode 100644 index 6b78c59a0..000000000 --- a/backend/utils/docker/compose.go +++ /dev/null @@ -1,87 +0,0 @@ -package docker - -import ( - "context" - "github.com/compose-spec/compose-go/v2/loader" - "github.com/compose-spec/compose-go/v2/types" - "github.com/docker/compose/v2/pkg/api" - "github.com/joho/godotenv" - "path" - "regexp" - "strings" -) - -type ComposeService struct { - api.Service - project *types.Project -} - -func GetComposeProject(projectName, workDir string, yml []byte, env []byte, skipNormalization bool) (*types.Project, error) { - var configFiles []types.ConfigFile - configFiles = append(configFiles, types.ConfigFile{ - Filename: "docker-compose.yml", - Content: yml}, - ) - envMap, err := godotenv.UnmarshalBytes(env) - if err != nil { - return nil, err - } - details := types.ConfigDetails{ - WorkingDir: workDir, - ConfigFiles: configFiles, - Environment: envMap, - } - projectName = strings.ToLower(projectName) - reg, _ := regexp.Compile(`[^a-z0-9_-]+`) - projectName = reg.ReplaceAllString(projectName, "") - project, err := loader.LoadWithContext(context.Background(), details, func(options *loader.Options) { - options.SetProjectName(projectName, true) - options.ResolvePaths = true - options.SkipNormalization = skipNormalization - }) - if err != nil { - return nil, err - } - project.ComposeFiles = []string{path.Join(workDir, "docker-compose.yml")} - return project, nil -} - -type ComposeProject struct { - Version string - Services map[string]Service `yaml:"services"` -} - -type Service struct { - Image string `yaml:"image"` -} - -func GetDockerComposeImages(projectName string, env, yml []byte) ([]string, error) { - var ( - configFiles []types.ConfigFile - images []string - ) - configFiles = append(configFiles, types.ConfigFile{ - Filename: "docker-compose.yml", - Content: yml}, - ) - envMap, err := godotenv.UnmarshalBytes(env) - if err != nil { - return nil, err - } - details := types.ConfigDetails{ - ConfigFiles: configFiles, - Environment: envMap, - } - - project, err := loader.LoadWithContext(context.Background(), details, func(options *loader.Options) { - options.SetProjectName(projectName, true) - options.ResolvePaths = true - }) - if err != nil { - return nil, err - } - for _, service := range project.AllServices() { - images = append(images, service.Image) - } - return images, nil -} diff --git a/backend/utils/docker/docker.go b/backend/utils/docker/docker.go deleted file mode 100644 index e11e42e1d..000000000 --- a/backend/utils/docker/docker.go +++ /dev/null @@ -1,177 +0,0 @@ -package docker - -import ( - "context" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/client" -) - -type Client struct { - cli *client.Client -} - -func NewClient() (Client, error) { - var settingItem model.Setting - _ = global.DB.Where("key = ?", "DockerSockPath").First(&settingItem).Error - if len(settingItem.Value) == 0 { - settingItem.Value = "unix:///var/run/docker.sock" - } - cli, err := client.NewClientWithOpts(client.FromEnv, client.WithHost(settingItem.Value), client.WithAPIVersionNegotiation()) - if err != nil { - return Client{}, err - } - - return Client{ - cli: cli, - }, nil -} - -func (c Client) Close() { - _ = c.cli.Close() -} - -func NewDockerClient() (*client.Client, error) { - var settingItem model.Setting - _ = global.DB.Where("key = ?", "DockerSockPath").First(&settingItem).Error - if len(settingItem.Value) == 0 { - settingItem.Value = "unix:///var/run/docker.sock" - } - cli, err := client.NewClientWithOpts(client.FromEnv, client.WithHost(settingItem.Value), client.WithAPIVersionNegotiation()) - if err != nil { - return nil, err - } - return cli, nil -} - -func (c Client) ListContainersByName(names []string) ([]types.Container, error) { - var ( - options container.ListOptions - namesMap = make(map[string]bool) - res []types.Container - ) - options.All = true - if len(names) > 0 { - var array []filters.KeyValuePair - for _, n := range names { - namesMap["/"+n] = true - array = append(array, filters.Arg("name", n)) - } - options.Filters = filters.NewArgs(array...) - } - containers, err := c.cli.ContainerList(context.Background(), options) - if err != nil { - return nil, err - } - for _, con := range containers { - if _, ok := namesMap[con.Names[0]]; ok { - res = append(res, con) - } - } - return res, nil -} -func (c Client) ListAllContainers() ([]types.Container, error) { - var ( - options container.ListOptions - ) - options.All = true - containers, err := c.cli.ContainerList(context.Background(), options) - if err != nil { - return nil, err - } - return containers, nil -} - -func (c Client) CreateNetwork(name string) error { - _, err := c.cli.NetworkCreate(context.Background(), name, types.NetworkCreate{ - Driver: "bridge", - }) - return err -} - -func (c Client) DeleteImage(imageID string) error { - if _, err := c.cli.ImageRemove(context.Background(), imageID, image.RemoveOptions{Force: true}); err != nil { - return err - } - return nil -} - -func (c Client) InspectContainer(containerID string) (types.ContainerJSON, error) { - return c.cli.ContainerInspect(context.Background(), containerID) -} - -func (c Client) PullImage(imageName string, force bool) error { - if !force { - exist, err := c.CheckImageExist(imageName) - if err != nil { - return err - } - if exist { - return nil - } - } - if _, err := c.cli.ImagePull(context.Background(), imageName, image.PullOptions{}); err != nil { - return err - } - return nil -} - -func (c Client) GetImageIDByName(imageName string) (string, error) { - filter := filters.NewArgs() - filter.Add("reference", imageName) - list, err := c.cli.ImageList(context.Background(), image.ListOptions{ - Filters: filter, - }) - if err != nil { - return "", err - } - if len(list) > 0 { - return list[0].ID, nil - } - return "", nil -} - -func (c Client) CheckImageExist(imageName string) (bool, error) { - filter := filters.NewArgs() - filter.Add("reference", imageName) - list, err := c.cli.ImageList(context.Background(), image.ListOptions{ - Filters: filter, - }) - if err != nil { - return false, err - } - return len(list) > 0, nil -} - -func (c Client) NetworkExist(name string) bool { - var options types.NetworkListOptions - options.Filters = filters.NewArgs(filters.Arg("name", name)) - networks, err := c.cli.NetworkList(context.Background(), options) - if err != nil { - return false - } - return len(networks) > 0 -} - -func CreateDefaultDockerNetwork() error { - cli, err := NewClient() - if err != nil { - global.LOG.Errorf("init docker client error %s", err.Error()) - return err - } - defer cli.Close() - if !cli.NetworkExist("1panel-network") { - if err := cli.CreateNetwork("1panel-network"); err != nil { - global.LOG.Errorf("create default docker network error %s", err.Error()) - return err - } - } - return nil -} diff --git a/backend/utils/encrypt/encrypt.go b/backend/utils/encrypt/encrypt.go deleted file mode 100644 index 62d4e1cde..000000000 --- a/backend/utils/encrypt/encrypt.go +++ /dev/null @@ -1,104 +0,0 @@ -package encrypt - -import ( - "bytes" - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "encoding/base64" - "fmt" - "io" - - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" -) - -func StringEncrypt(text string) (string, error) { - if len(text) == 0 { - return "", nil - } - if len(global.CONF.System.EncryptKey) == 0 { - var encryptSetting model.Setting - if err := global.DB.Where("key = ?", "EncryptKey").First(&encryptSetting).Error; err != nil { - return "", err - } - global.CONF.System.EncryptKey = encryptSetting.Value - } - key := global.CONF.System.EncryptKey - pass := []byte(text) - xpass, err := aesEncryptWithSalt([]byte(key), pass) - if err == nil { - pass64 := base64.StdEncoding.EncodeToString(xpass) - return pass64, err - } - return "", err -} - -func StringDecrypt(text string) (string, error) { - if len(text) == 0 { - return "", nil - } - if len(global.CONF.System.EncryptKey) == 0 { - var encryptSetting model.Setting - if err := global.DB.Where("key = ?", "EncryptKey").First(&encryptSetting).Error; err != nil { - return "", err - } - global.CONF.System.EncryptKey = encryptSetting.Value - } - key := global.CONF.System.EncryptKey - bytesPass, err := base64.StdEncoding.DecodeString(text) - if err != nil { - return "", err - } - var tpass []byte - tpass, err = aesDecryptWithSalt([]byte(key), bytesPass) - if err == nil { - result := string(tpass[:]) - return result, err - } - return "", err -} - -func padding(plaintext []byte, blockSize int) []byte { - padding := blockSize - len(plaintext)%blockSize - padtext := bytes.Repeat([]byte{byte(padding)}, padding) - return append(plaintext, padtext...) -} - -func unPadding(origData []byte) []byte { - length := len(origData) - unpadding := int(origData[length-1]) - return origData[:(length - unpadding)] -} - -func aesEncryptWithSalt(key, plaintext []byte) ([]byte, error) { - plaintext = padding(plaintext, aes.BlockSize) - block, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - ciphertext := make([]byte, aes.BlockSize+len(plaintext)) - iv := ciphertext[0:aes.BlockSize] - if _, err := io.ReadFull(rand.Reader, iv); err != nil { - return nil, err - } - cbc := cipher.NewCBCEncrypter(block, iv) - cbc.CryptBlocks(ciphertext[aes.BlockSize:], plaintext) - return ciphertext, nil -} -func aesDecryptWithSalt(key, ciphertext []byte) ([]byte, error) { - var block cipher.Block - block, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - if len(ciphertext) < aes.BlockSize { - return nil, fmt.Errorf("iciphertext too short") - } - iv := ciphertext[:aes.BlockSize] - ciphertext = ciphertext[aes.BlockSize:] - cbc := cipher.NewCBCDecrypter(block, iv) - cbc.CryptBlocks(ciphertext, ciphertext) - ciphertext = unPadding(ciphertext) - return ciphertext, nil -} diff --git a/backend/utils/env/env.go b/backend/utils/env/env.go deleted file mode 100644 index 630511618..000000000 --- a/backend/utils/env/env.go +++ /dev/null @@ -1,52 +0,0 @@ -package env - -import ( - "fmt" - "github.com/joho/godotenv" - "os" - "sort" - "strconv" - "strings" -) - -func Write(envMap map[string]string, filename string) error { - content, err := Marshal(envMap) - if err != nil { - return err - } - file, err := os.Create(filename) - if err != nil { - return err - } - defer file.Close() - _, err = file.WriteString(content + "\n") - if err != nil { - return err - } - return file.Sync() -} - -func Marshal(envMap map[string]string) (string, error) { - lines := make([]string, 0, len(envMap)) - for k, v := range envMap { - if d, err := strconv.Atoi(v); err == nil { - lines = append(lines, fmt.Sprintf(`%s=%d`, k, d)) - } else { - lines = append(lines, fmt.Sprintf(`%s="%s"`, k, v)) - } - } - sort.Strings(lines) - return strings.Join(lines, "\n"), nil -} - -func GetEnvValueByKey(envPath, key string) (string, error) { - envMap, err := godotenv.Read(envPath) - if err != nil { - return "", err - } - value, ok := envMap[key] - if !ok { - return "", fmt.Errorf("key %s not found in %s", key, envPath) - } - return value, nil -} diff --git a/backend/utils/files/archiver.go b/backend/utils/files/archiver.go deleted file mode 100644 index 374b500e7..000000000 --- a/backend/utils/files/archiver.go +++ /dev/null @@ -1,38 +0,0 @@ -package files - -import ( - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" -) - -type ShellArchiver interface { - Extract(filePath, dstDir string, secret string) error - Compress(sourcePaths []string, dstFile string, secret string) error -} - -func NewShellArchiver(compressType CompressType) (ShellArchiver, error) { - switch compressType { - case Tar: - if err := checkCmdAvailability("tar"); err != nil { - return nil, err - } - return NewTarArchiver(compressType), nil - case TarGz: - return NewTarGzArchiver(), nil - case Zip: - if err := checkCmdAvailability("zip"); err != nil { - return nil, err - } - return NewZipArchiver(), nil - default: - return nil, buserr.New("unsupported compress type") - } -} - -func checkCmdAvailability(cmdStr string) error { - if cmd.Which(cmdStr) { - return nil - } - return buserr.WithName(constant.ErrCmdNotFound, cmdStr) -} diff --git a/backend/utils/files/file_op.go b/backend/utils/files/file_op.go deleted file mode 100644 index 913a65d37..000000000 --- a/backend/utils/files/file_op.go +++ /dev/null @@ -1,666 +0,0 @@ -package files - -import ( - "archive/zip" - "bufio" - "bytes" - "context" - "crypto/tls" - "encoding/json" - "fmt" - "io" - "io/fs" - "net/http" - "os" - "path" - "path/filepath" - "strconv" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - http2 "github.com/1Panel-dev/1Panel/backend/utils/http" - cZip "github.com/klauspost/compress/zip" - "golang.org/x/text/encoding/simplifiedchinese" - "golang.org/x/text/transform" - - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/mholt/archiver/v4" - "github.com/pkg/errors" - "github.com/spf13/afero" -) - -type FileOp struct { - Fs afero.Fs -} - -func NewFileOp() FileOp { - return FileOp{ - Fs: afero.NewOsFs(), - } -} - -func (f FileOp) OpenFile(dst string) (fs.File, error) { - return f.Fs.Open(dst) -} - -func (f FileOp) GetContent(dst string) ([]byte, error) { - afs := &afero.Afero{Fs: f.Fs} - cByte, err := afs.ReadFile(dst) - if err != nil { - return nil, err - } - return cByte, nil -} - -func (f FileOp) CreateDir(dst string, mode fs.FileMode) error { - return f.Fs.MkdirAll(dst, mode) -} - -func (f FileOp) CreateDirWithMode(dst string, mode fs.FileMode) error { - if err := f.Fs.MkdirAll(dst, mode); err != nil { - return err - } - return f.ChmodRWithMode(dst, mode, true) -} - -func (f FileOp) CreateFile(dst string) error { - if _, err := f.Fs.Create(dst); err != nil { - return err - } - return nil -} - -func (f FileOp) CreateFileWithMode(dst string, mode fs.FileMode) error { - file, err := f.Fs.OpenFile(dst, os.O_CREATE, mode) - if err != nil { - return err - } - return file.Close() -} - -func (f FileOp) LinkFile(source string, dst string, isSymlink bool) error { - if isSymlink { - osFs := afero.OsFs{} - return osFs.SymlinkIfPossible(source, dst) - } else { - return os.Link(source, dst) - } -} - -func (f FileOp) DeleteDir(dst string) error { - return f.Fs.RemoveAll(dst) -} - -func (f FileOp) Stat(dst string) bool { - info, _ := f.Fs.Stat(dst) - return info != nil -} - -func (f FileOp) DeleteFile(dst string) error { - return f.Fs.Remove(dst) -} - -func (f FileOp) CleanDir(dst string) error { - return cmd.ExecCmd(fmt.Sprintf("rm -rf %s/*", dst)) -} - -func (f FileOp) RmRf(dst string) error { - return cmd.ExecCmd(fmt.Sprintf("rm -rf %s", dst)) -} - -func (f FileOp) WriteFile(dst string, in io.Reader, mode fs.FileMode) error { - file, err := f.Fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode) - if err != nil { - return err - } - defer file.Close() - - if _, err = io.Copy(file, in); err != nil { - return err - } - - if _, err = file.Stat(); err != nil { - return err - } - return nil -} - -func (f FileOp) SaveFile(dst string, content string, mode fs.FileMode) error { - if !f.Stat(path.Dir(dst)) { - _ = f.CreateDir(path.Dir(dst), mode.Perm()) - } - file, err := f.Fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode) - if err != nil { - return err - } - defer file.Close() - write := bufio.NewWriter(file) - _, _ = write.WriteString(content) - write.Flush() - return nil -} - -func (f FileOp) SaveFileWithByte(dst string, content []byte, mode fs.FileMode) error { - if !f.Stat(path.Dir(dst)) { - _ = f.CreateDir(path.Dir(dst), mode.Perm()) - } - file, err := f.Fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode) - if err != nil { - return err - } - defer file.Close() - write := bufio.NewWriter(file) - _, _ = write.Write(content) - write.Flush() - return nil -} - -func (f FileOp) ChownR(dst string, uid string, gid string, sub bool) error { - cmdStr := fmt.Sprintf(`chown %s:%s "%s"`, uid, gid, dst) - if sub { - cmdStr = fmt.Sprintf(`chown -R %s:%s "%s"`, uid, gid, dst) - } - if cmd.HasNoPasswordSudo() { - cmdStr = fmt.Sprintf("sudo %s", cmdStr) - } - if msg, err := cmd.ExecWithTimeOut(cmdStr, 10*time.Second); err != nil { - if msg != "" { - return errors.New(msg) - } - return err - } - return nil -} - -func (f FileOp) ChmodR(dst string, mode int64, sub bool) error { - cmdStr := fmt.Sprintf(`chmod %v "%s"`, fmt.Sprintf("%04o", mode), dst) - if sub { - cmdStr = fmt.Sprintf(`chmod -R %v "%s"`, fmt.Sprintf("%04o", mode), dst) - } - if cmd.HasNoPasswordSudo() { - cmdStr = fmt.Sprintf("sudo %s", cmdStr) - } - if msg, err := cmd.ExecWithTimeOut(cmdStr, 10*time.Second); err != nil { - if msg != "" { - return errors.New(msg) - } - return err - } - return nil -} - -func (f FileOp) ChmodRWithMode(dst string, mode fs.FileMode, sub bool) error { - cmdStr := fmt.Sprintf(`chmod %v "%s"`, fmt.Sprintf("%o", mode.Perm()), dst) - if sub { - cmdStr = fmt.Sprintf(`chmod -R %v "%s"`, fmt.Sprintf("%o", mode.Perm()), dst) - } - if cmd.HasNoPasswordSudo() { - cmdStr = fmt.Sprintf("sudo %s", cmdStr) - } - if msg, err := cmd.ExecWithTimeOut(cmdStr, 10*time.Second); err != nil { - if msg != "" { - return errors.New(msg) - } - return err - } - return nil -} - -func (f FileOp) Rename(oldName string, newName string) error { - return f.Fs.Rename(oldName, newName) -} - -type WriteCounter struct { - Total uint64 - Written uint64 - Key string - Name string -} - -type Process struct { - Total uint64 `json:"total"` - Written uint64 `json:"written"` - Percent float64 `json:"percent"` - Name string `json:"name"` -} - -func (w *WriteCounter) Write(p []byte) (n int, err error) { - n = len(p) - w.Written += uint64(n) - w.SaveProcess() - return n, nil -} - -func (w *WriteCounter) SaveProcess() { - percentValue := 0.0 - if w.Total > 0 { - percent := float64(w.Written) / float64(w.Total) * 100 - percentValue, _ = strconv.ParseFloat(fmt.Sprintf("%.2f", percent), 64) - } - process := Process{ - Total: w.Total, - Written: w.Written, - Percent: percentValue, - Name: w.Name, - } - by, _ := json.Marshal(process) - if percentValue < 100 { - if err := global.CACHE.Set(w.Key, string(by)); err != nil { - global.LOG.Errorf("save cache error, err %s", err.Error()) - } - } else { - if err := global.CACHE.SetWithTTL(w.Key, string(by), time.Second*time.Duration(10)); err != nil { - global.LOG.Errorf("save cache error, err %s", err.Error()) - } - } -} - -func (f FileOp) DownloadFileWithProcess(url, dst, key string, ignoreCertificate bool) error { - client := &http.Client{} - if ignoreCertificate { - client.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - } - request, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil - } - request.Header.Set("Accept-Encoding", "identity") - resp, err := client.Do(request) - if err != nil { - global.LOG.Errorf("get download file [%s] error, err %s", dst, err.Error()) - return err - } - out, err := os.Create(dst) - if err != nil { - global.LOG.Errorf("create download file [%s] error, err %s", dst, err.Error()) - return err - } - go func() { - counter := &WriteCounter{} - counter.Key = key - if resp.ContentLength > 0 { - counter.Total = uint64(resp.ContentLength) - } - counter.Name = filepath.Base(dst) - if _, err = io.Copy(out, io.TeeReader(resp.Body, counter)); err != nil { - global.LOG.Errorf("save download file [%s] error, err %s", dst, err.Error()) - } - out.Close() - resp.Body.Close() - - value, err := global.CACHE.Get(counter.Key) - if err != nil { - global.LOG.Errorf("get cache error,err %s", err.Error()) - return - } - process := &Process{} - _ = json.Unmarshal(value, process) - process.Percent = 100 - process.Name = counter.Name - process.Total = process.Written - by, _ := json.Marshal(process) - if err := global.CACHE.SetWithTTL(counter.Key, string(by), time.Second*time.Duration(10)); err != nil { - global.LOG.Errorf("save cache error, err %s", err.Error()) - } - }() - return nil -} - -func (f FileOp) DownloadFile(url, dst string) error { - resp, err := http2.GetHttpRes(url) - if err != nil { - return err - } - defer resp.Body.Close() - - out, err := os.Create(dst) - if err != nil { - return fmt.Errorf("create download file [%s] error, err %s", dst, err.Error()) - } - defer out.Close() - - if _, err = io.Copy(out, resp.Body); err != nil { - return fmt.Errorf("save download file [%s] error, err %s", dst, err.Error()) - } - return nil -} - -func (f FileOp) DownloadFileWithProxy(url, dst string) error { - _, resp, err := http2.HandleGet(url, http.MethodGet, constant.TimeOut5m) - if err != nil { - return err - } - - out, err := os.Create(dst) - if err != nil { - return fmt.Errorf("create download file [%s] error, err %s", dst, err.Error()) - } - defer out.Close() - - reader := bytes.NewReader(resp) - if _, err = io.Copy(out, reader); err != nil { - return fmt.Errorf("save download file [%s] error, err %s", dst, err.Error()) - } - return nil -} - -func (f FileOp) Cut(oldPaths []string, dst, name string, cover bool) error { - for _, p := range oldPaths { - var dstPath string - if name != "" { - dstPath = filepath.Join(dst, name) - if f.Stat(dstPath) { - dstPath = dst - } - } else { - base := filepath.Base(p) - dstPath = filepath.Join(dst, base) - } - coverFlag := "" - if cover { - coverFlag = "-f" - } - - cmdStr := fmt.Sprintf(`mv %s '%s' '%s'`, coverFlag, p, dstPath) - if err := cmd.ExecCmd(cmdStr); err != nil { - return err - } - } - return nil -} - -func (f FileOp) Mv(oldPath, dstPath string) error { - cmdStr := fmt.Sprintf(`mv '%s' '%s'`, oldPath, dstPath) - if err := cmd.ExecCmd(cmdStr); err != nil { - return err - } - return nil -} - -func (f FileOp) Copy(src, dst string) error { - if src = path.Clean("/" + src); src == "" { - return os.ErrNotExist - } - if dst = path.Clean("/" + dst); dst == "" { - return os.ErrNotExist - } - if src == "/" || dst == "/" { - return os.ErrInvalid - } - if dst == src { - return os.ErrInvalid - } - info, err := f.Fs.Stat(src) - if err != nil { - return err - } - if info.IsDir() { - return f.CopyDir(src, dst) - } - return f.CopyFile(src, dst) -} - -func (f FileOp) CopyAndReName(src, dst, name string, cover bool) error { - if src = path.Clean("/" + src); src == "" { - return os.ErrNotExist - } - if dst = path.Clean("/" + dst); dst == "" { - return os.ErrNotExist - } - if src == "/" || dst == "/" { - return os.ErrInvalid - } - if dst == src { - return os.ErrInvalid - } - - srcInfo, err := f.Fs.Stat(src) - if err != nil { - return err - } - - if srcInfo.IsDir() { - dstPath := dst - if name != "" && !cover { - dstPath = filepath.Join(dst, name) - } - return cmd.ExecCmd(fmt.Sprintf(`cp -rf '%s' '%s'`, src, dstPath)) - } else { - dstPath := filepath.Join(dst, name) - if cover { - dstPath = dst - } - return cmd.ExecCmd(fmt.Sprintf(`cp -f '%s' '%s'`, src, dstPath)) - } -} - -func (f FileOp) CopyDir(src, dst string) error { - srcInfo, err := f.Fs.Stat(src) - if err != nil { - return err - } - dstDir := filepath.Join(dst, srcInfo.Name()) - if err = f.Fs.MkdirAll(dstDir, srcInfo.Mode()); err != nil { - return err - } - return cmd.ExecCmd(fmt.Sprintf(`cp -rf '%s' '%s'`, src, dst+"/")) -} - -func (f FileOp) CopyFile(src, dst string) error { - dst = filepath.Clean(dst) + string(filepath.Separator) - return cmd.ExecCmd(fmt.Sprintf(`cp -f '%s' '%s'`, src, dst+"/")) -} - -func (f FileOp) GetDirSize(path string) (float64, error) { - var size int64 - err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if !info.IsDir() { - size += info.Size() - } - return nil - }) - if err != nil { - return 0, err - } - return float64(size), nil -} - -func getFormat(cType CompressType) archiver.CompressedArchive { - format := archiver.CompressedArchive{} - switch cType { - case Tar: - format.Archival = archiver.Tar{} - case TarGz, Gz: - format.Compression = archiver.Gz{} - format.Archival = archiver.Tar{} - case SdkTarGz: - format.Compression = archiver.Gz{} - format.Archival = archiver.Tar{} - case SdkZip, Zip: - format.Archival = archiver.Zip{ - Compression: zip.Deflate, - } - case Bz2: - format.Compression = archiver.Bz2{} - format.Archival = archiver.Tar{} - case Xz: - format.Compression = archiver.Xz{} - format.Archival = archiver.Tar{} - } - return format -} - -func (f FileOp) Compress(srcRiles []string, dst string, name string, cType CompressType, secret string) error { - format := getFormat(cType) - - fileMaps := make(map[string]string, len(srcRiles)) - for _, s := range srcRiles { - base := filepath.Base(s) - fileMaps[s] = base - } - - if !f.Stat(dst) { - _ = f.CreateDir(dst, 0755) - } - - files, err := archiver.FilesFromDisk(nil, fileMaps) - if err != nil { - return err - } - dstFile := filepath.Join(dst, name) - out, err := f.Fs.Create(dstFile) - if err != nil { - return err - } - - switch cType { - case Zip: - if err := ZipFile(files, out); err == nil { - return nil - } - _ = f.DeleteFile(dstFile) - return NewZipArchiver().Compress(srcRiles, dstFile, "") - case TarGz: - err = NewTarGzArchiver().Compress(srcRiles, dstFile, secret) - if err != nil { - _ = f.DeleteFile(dstFile) - return err - } - default: - err = format.Archive(context.Background(), out, files) - if err != nil { - _ = f.DeleteFile(dstFile) - return err - } - } - return nil -} - -func isIgnoreFile(name string) bool { - return strings.HasPrefix(name, "__MACOSX") || strings.HasSuffix(name, ".DS_Store") || strings.HasPrefix(name, "._") -} - -func decodeGBK(input string) (string, error) { - decoder := simplifiedchinese.GBK.NewDecoder() - decoded, _, err := transform.String(decoder, input) - if err != nil { - return "", err - } - return decoded, nil -} - -func (f FileOp) decompressWithSDK(srcFile string, dst string, cType CompressType) error { - format := getFormat(cType) - handler := func(ctx context.Context, archFile archiver.File) error { - info := archFile.FileInfo - if isIgnoreFile(archFile.Name()) { - return nil - } - fileName := archFile.NameInArchive - var err error - if header, ok := archFile.Header.(cZip.FileHeader); ok { - if header.NonUTF8 && header.Flags == 0 { - fileName, err = decodeGBK(fileName) - if err != nil { - return err - } - } - } - filePath := filepath.Join(dst, fileName) - if archFile.FileInfo.IsDir() { - if err := f.Fs.MkdirAll(filePath, info.Mode()); err != nil { - return err - } - return nil - } else { - parentDir := path.Dir(filePath) - if !f.Stat(parentDir) { - if err := f.Fs.MkdirAll(parentDir, info.Mode()); err != nil { - return err - } - } - } - fr, err := archFile.Open() - if err != nil { - return err - } - defer fr.Close() - fw, err := f.Fs.OpenFile(filePath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, info.Mode()) - if err != nil { - return err - } - defer fw.Close() - if _, err := io.Copy(fw, fr); err != nil { - return err - } - - return nil - } - input, err := f.Fs.Open(srcFile) - if err != nil { - return err - } - return format.Extract(context.Background(), input, nil, handler) -} - -func (f FileOp) Decompress(srcFile string, dst string, cType CompressType, secret string) error { - if cType == Tar || cType == Zip || cType == TarGz { - shellArchiver, err := NewShellArchiver(cType) - if err == nil { - if err = shellArchiver.Extract(srcFile, dst, secret); err == nil { - return nil - } - } - } - return f.decompressWithSDK(srcFile, dst, cType) -} - -func ZipFile(files []archiver.File, dst afero.File) error { - zw := zip.NewWriter(dst) - defer zw.Close() - - for _, file := range files { - hdr, err := zip.FileInfoHeader(file) - if err != nil { - return err - } - hdr.Method = zip.Deflate - hdr.Name = file.NameInArchive - if file.IsDir() { - if !strings.HasSuffix(hdr.Name, "/") { - hdr.Name += "/" - } - } - w, err := zw.CreateHeader(hdr) - if err != nil { - return err - } - if file.IsDir() { - continue - } - - if file.LinkTarget != "" { - _, err = w.Write([]byte(filepath.ToSlash(file.LinkTarget))) - if err != nil { - return err - } - } else { - fileReader, err := file.Open() - if err != nil { - return err - } - _, err = io.Copy(w, fileReader) - if err != nil { - return err - } - } - } - return nil -} diff --git a/backend/utils/files/fileinfo.go b/backend/utils/files/fileinfo.go deleted file mode 100644 index 660d20fe6..000000000 --- a/backend/utils/files/fileinfo.go +++ /dev/null @@ -1,425 +0,0 @@ -package files - -import ( - "bufio" - "fmt" - "github.com/1Panel-dev/1Panel/backend/app/repo" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "io/fs" - "net/http" - "os" - "os/exec" - "path" - "path/filepath" - "sort" - "strconv" - "strings" - "syscall" - "time" - - "github.com/spf13/afero" -) - -type FileInfo struct { - Fs afero.Fs `json:"-"` - Path string `json:"path"` - Name string `json:"name"` - User string `json:"user"` - Group string `json:"group"` - Uid string `json:"uid"` - Gid string `json:"gid"` - Extension string `json:"extension"` - Content string `json:"content"` - Size int64 `json:"size"` - IsDir bool `json:"isDir"` - IsSymlink bool `json:"isSymlink"` - IsHidden bool `json:"isHidden"` - LinkPath string `json:"linkPath"` - Type string `json:"type"` - Mode string `json:"mode"` - MimeType string `json:"mimeType"` - UpdateTime time.Time `json:"updateTime"` - ModTime time.Time `json:"modTime"` - FileMode os.FileMode `json:"-"` - Items []*FileInfo `json:"items"` - ItemTotal int `json:"itemTotal"` - FavoriteID uint `json:"favoriteID"` - IsDetail bool `json:"isDetail"` -} - -type FileOption struct { - Path string `json:"path"` - Search string `json:"search"` - ContainSub bool `json:"containSub"` - Expand bool `json:"expand"` - Dir bool `json:"dir"` - ShowHidden bool `json:"showHidden"` - Page int `json:"page"` - PageSize int `json:"pageSize"` - SortBy string `json:"sortBy"` - SortOrder string `json:"sortOrder"` - IsDetail bool `json:"isDetail"` -} - -type FileSearchInfo struct { - Path string `json:"path"` - fs.FileInfo -} - -func NewFileInfo(op FileOption) (*FileInfo, error) { - var appFs = afero.NewOsFs() - - info, err := appFs.Stat(op.Path) - if err != nil { - return nil, err - } - - file := &FileInfo{ - Fs: appFs, - Path: op.Path, - Name: info.Name(), - IsDir: info.IsDir(), - FileMode: info.Mode(), - ModTime: info.ModTime(), - Size: info.Size(), - IsSymlink: IsSymlink(info.Mode()), - Extension: filepath.Ext(info.Name()), - IsHidden: IsHidden(op.Path), - Mode: fmt.Sprintf("%04o", info.Mode().Perm()), - User: GetUsername(info.Sys().(*syscall.Stat_t).Uid), - Uid: strconv.FormatUint(uint64(info.Sys().(*syscall.Stat_t).Uid), 10), - Gid: strconv.FormatUint(uint64(info.Sys().(*syscall.Stat_t).Gid), 10), - Group: GetGroup(info.Sys().(*syscall.Stat_t).Gid), - MimeType: GetMimeType(op.Path), - IsDetail: op.IsDetail, - } - favoriteRepo := repo.NewIFavoriteRepo() - favorite, _ := favoriteRepo.GetFirst(favoriteRepo.WithByPath(op.Path)) - if favorite.ID > 0 { - file.FavoriteID = favorite.ID - } - - if file.IsSymlink { - file.LinkPath = GetSymlink(op.Path) - } - if op.Expand { - if err := handleExpansion(file, op); err != nil { - return nil, err - } - } - return file, nil -} - -func handleExpansion(file *FileInfo, op FileOption) error { - if file.IsDir { - return file.listChildren(op) - } - - if !file.IsDetail { - return file.getContent() - } - - return nil -} - -func (f *FileInfo) search(search string, count int) (files []FileSearchInfo, total int, err error) { - cmd := exec.Command("find", f.Path, "-name", fmt.Sprintf("*%s*", search)) - output, err := cmd.StdoutPipe() - if err != nil { - return - } - if err = cmd.Start(); err != nil { - return - } - defer func() { - _ = cmd.Wait() - _ = cmd.Process.Kill() - }() - - scanner := bufio.NewScanner(output) - for scanner.Scan() { - line := scanner.Text() - info, err := os.Stat(line) - if err != nil { - continue - } - total++ - if total > count { - continue - } - files = append(files, FileSearchInfo{ - Path: line, - FileInfo: info, - }) - } - if err = scanner.Err(); err != nil { - return - } - return -} - -func sortFileList(list []FileSearchInfo, sortBy, sortOrder string) { - switch sortBy { - case "name": - if sortOrder == "ascending" { - sort.Slice(list, func(i, j int) bool { - return list[i].Name() < list[j].Name() - }) - } else { - sort.Slice(list, func(i, j int) bool { - return list[i].Name() > list[j].Name() - }) - } - case "size": - if sortOrder == "ascending" { - sort.Slice(list, func(i, j int) bool { - return list[i].Size() < list[j].Size() - }) - } else { - sort.Slice(list, func(i, j int) bool { - return list[i].Size() > list[j].Size() - }) - } - case "modTime": - if sortOrder == "ascending" { - sort.Slice(list, func(i, j int) bool { - return list[i].ModTime().Before(list[j].ModTime()) - }) - } else { - sort.Slice(list, func(i, j int) bool { - return list[i].ModTime().After(list[j].ModTime()) - }) - } - } -} - -func (f *FileInfo) listChildren(option FileOption) error { - afs := &afero.Afero{Fs: f.Fs} - var ( - files []FileSearchInfo - err error - total int - ) - - if option.Search != "" && option.ContainSub { - files, total, err = f.search(option.Search, option.Page*option.PageSize) - if err != nil { - return err - } - } else { - files, err = f.getFiles(afs, option) - if err != nil { - return err - } - } - - items, err := f.processFiles(files, option) - if err != nil { - return err - } - - if option.ContainSub { - f.ItemTotal = total - } - start := (option.Page - 1) * option.PageSize - end := option.PageSize + start - var result []*FileInfo - if start < 0 || start > f.ItemTotal || end < 0 || start > end { - result = items - } else { - if end > f.ItemTotal { - result = items[start:] - } else { - result = items[start:end] - } - } - - f.Items = result - return nil -} - -func (f *FileInfo) getFiles(afs *afero.Afero, option FileOption) ([]FileSearchInfo, error) { - dirFiles, err := afs.ReadDir(f.Path) - if err != nil { - return nil, err - } - - var ( - dirs []FileSearchInfo - fileList []FileSearchInfo - ) - - for _, file := range dirFiles { - info := FileSearchInfo{ - Path: f.Path, - FileInfo: file, - } - if file.IsDir() { - dirs = append(dirs, info) - } else { - fileList = append(fileList, info) - } - } - - sortFileList(dirs, option.SortBy, option.SortOrder) - sortFileList(fileList, option.SortBy, option.SortOrder) - - return append(dirs, fileList...), nil -} - -func (f *FileInfo) processFiles(files []FileSearchInfo, option FileOption) ([]*FileInfo, error) { - var items []*FileInfo - - for _, df := range files { - if shouldSkipFile(df, option) { - continue - } - - name, fPath := f.getFilePathAndName(option, df) - - if !option.ShowHidden && IsHidden(name) { - continue - } - f.ItemTotal++ - - isSymlink, isInvalidLink := f.checkSymlink(df) - - file := &FileInfo{ - Fs: f.Fs, - Name: name, - Size: df.Size(), - ModTime: df.ModTime(), - FileMode: df.Mode(), - IsDir: df.IsDir(), - IsSymlink: isSymlink, - IsHidden: IsHidden(fPath), - Extension: filepath.Ext(name), - Path: fPath, - Mode: fmt.Sprintf("%04o", df.Mode().Perm()), - User: GetUsername(df.Sys().(*syscall.Stat_t).Uid), - Group: GetGroup(df.Sys().(*syscall.Stat_t).Gid), - Uid: strconv.FormatUint(uint64(df.Sys().(*syscall.Stat_t).Uid), 10), - Gid: strconv.FormatUint(uint64(df.Sys().(*syscall.Stat_t).Gid), 10), - } - favoriteRepo := repo.NewIFavoriteRepo() - favorite, _ := favoriteRepo.GetFirst(favoriteRepo.WithByPath(fPath)) - if favorite.ID > 0 { - file.FavoriteID = favorite.ID - } - if isSymlink { - file.LinkPath = GetSymlink(fPath) - } - if df.Size() > 0 { - file.MimeType = GetMimeType(fPath) - } - if isInvalidLink { - file.Type = "invalid_link" - } - items = append(items, file) - } - - return items, nil -} - -func shouldSkipFile(df FileSearchInfo, option FileOption) bool { - if option.Dir && !df.IsDir() { - return true - } - - if option.Search != "" && !option.ContainSub { - lowerName := strings.ToLower(df.Name()) - lowerSearch := strings.ToLower(option.Search) - if !strings.Contains(lowerName, lowerSearch) { - return true - } - } - - return false -} - -func (f *FileInfo) getFilePathAndName(option FileOption, df FileSearchInfo) (string, string) { - name := df.Name() - fPath := path.Join(df.Path, df.Name()) - - if option.Search != "" && option.ContainSub { - fPath = df.Path - name = strings.TrimPrefix(strings.TrimPrefix(fPath, f.Path), "/") - } - - return name, fPath -} - -func (f *FileInfo) checkSymlink(df FileSearchInfo) (bool, bool) { - isSymlink := false - isInvalidLink := false - - if IsSymlink(df.Mode()) { - isSymlink = true - info, err := f.Fs.Stat(path.Join(df.Path, df.Name())) - if err == nil { - df.FileInfo = info - } else { - isInvalidLink = true - } - } - - return isSymlink, isInvalidLink -} - -func (f *FileInfo) getContent() error { - if IsBlockDevice(f.FileMode) { - return buserr.New(constant.ErrFileCanNotRead) - } - if f.Size > 10*1024*1024 { - return buserr.New("ErrFileToLarge") - } - afs := &afero.Afero{Fs: f.Fs} - cByte, err := afs.ReadFile(f.Path) - if err != nil { - return nil - } - if len(cByte) > 0 && DetectBinary(cByte) { - return buserr.New(constant.ErrFileCanNotRead) - } - f.Content = string(cByte) - return nil -} - -func DetectBinary(buf []byte) bool { - mimeType := http.DetectContentType(buf) - if !strings.HasPrefix(mimeType, "text/") { - whiteByte := 0 - n := min(1024, len(buf)) - for i := 0; i < n; i++ { - if (buf[i] >= 0x20) || buf[i] == 9 || buf[i] == 10 || buf[i] == 13 { - whiteByte++ - } else if buf[i] <= 6 || (buf[i] >= 14 && buf[i] <= 31) { - return true - } - } - return whiteByte < 1 - } - return false - -} - -func min(x, y int) int { - if x < y { - return x - } - return y -} - -type CompressType string - -const ( - Zip CompressType = "zip" - Gz CompressType = "gz" - Bz2 CompressType = "bz2" - Tar CompressType = "tar" - TarGz CompressType = "tar.gz" - Xz CompressType = "xz" - SdkZip CompressType = "sdkZip" - SdkTarGz CompressType = "sdkTarGz" -) diff --git a/backend/utils/files/tar.go b/backend/utils/files/tar.go deleted file mode 100644 index 6aaa1f035..000000000 --- a/backend/utils/files/tar.go +++ /dev/null @@ -1,38 +0,0 @@ -package files - -import ( - "fmt" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" -) - -type TarArchiver struct { - Cmd string - CompressType CompressType -} - -func NewTarArchiver(compressType CompressType) ShellArchiver { - return &TarArchiver{ - Cmd: "tar", - CompressType: compressType, - } -} - -func (t TarArchiver) Extract(FilePath string, dstDir string, secret string) error { - return cmd.ExecCmd(fmt.Sprintf("%s %s %s -C %s", t.Cmd, t.getOptionStr("extract"), FilePath, dstDir)) -} - -func (t TarArchiver) Compress(sourcePaths []string, dstFile string, secret string) error { - return nil -} - -func (t TarArchiver) getOptionStr(Option string) string { - switch t.CompressType { - case Tar: - if Option == "compress" { - return "cvf" - } else { - return "xf" - } - } - return "" -} diff --git a/backend/utils/files/tar_gz.go b/backend/utils/files/tar_gz.go deleted file mode 100644 index 8a1691abf..000000000 --- a/backend/utils/files/tar_gz.go +++ /dev/null @@ -1,61 +0,0 @@ -package files - -import ( - "fmt" - "path/filepath" - "strings" - - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" -) - -type TarGzArchiver struct { -} - -func NewTarGzArchiver() ShellArchiver { - return &TarGzArchiver{} -} - -func (t TarGzArchiver) Extract(filePath, dstDir string, secret string) error { - var err error - commands := "" - if len(secret) != 0 { - extraCmd := "openssl enc -d -aes-256-cbc -k '" + secret + "' -in " + filePath + " | " - commands = fmt.Sprintf("%s tar -zxvf - -C %s", extraCmd, dstDir+" > /dev/null 2>&1") - global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******")) - } else { - commands = fmt.Sprintf("tar -zxvf %s %s", filePath+" -C ", dstDir+" > /dev/null 2>&1") - global.LOG.Debug(commands) - } - if err = cmd.ExecCmd(commands); err != nil { - return err - } - return nil -} - -func (t TarGzArchiver) Compress(sourcePaths []string, dstFile string, secret string) error { - var err error - path := "" - itemDir := "" - for _, item := range sourcePaths { - itemDir += filepath.Base(item) + " " - } - aheadDir := dstFile[:strings.LastIndex(dstFile, "/")] - if len(aheadDir) == 0 { - aheadDir = "/" - } - path += fmt.Sprintf("- -C %s %s", aheadDir, itemDir) - commands := "" - if len(secret) != 0 { - extraCmd := "| openssl enc -aes-256-cbc -salt -k '" + secret + "' -out" - commands = fmt.Sprintf("tar -zcf %s %s %s", path, extraCmd, dstFile) - global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******")) - } else { - commands = fmt.Sprintf("tar -zcf %s -C %s %s", dstFile, aheadDir, itemDir) - global.LOG.Debug(commands) - } - if err = cmd.ExecCmd(commands); err != nil { - return err - } - return nil -} diff --git a/backend/utils/files/utils.go b/backend/utils/files/utils.go deleted file mode 100644 index 76ba78156..000000000 --- a/backend/utils/files/utils.go +++ /dev/null @@ -1,164 +0,0 @@ -package files - -import ( - "bufio" - "fmt" - "io" - "net/http" - "os" - "os/user" - "path/filepath" - "strconv" - "strings" -) - -func IsSymlink(mode os.FileMode) bool { - return mode&os.ModeSymlink != 0 -} - -func IsBlockDevice(mode os.FileMode) bool { - return mode&os.ModeDevice != 0 && mode&os.ModeCharDevice == 0 -} - -func GetMimeType(path string) string { - file, err := os.Open(path) - if err != nil { - return "" - } - defer file.Close() - - buffer := make([]byte, 512) - _, err = file.Read(buffer) - if err != nil { - return "" - } - mimeType := http.DetectContentType(buffer) - return mimeType -} - -func GetSymlink(path string) string { - linkPath, err := os.Readlink(path) - if err != nil { - return "" - } - return linkPath -} - -func GetUsername(uid uint32) string { - usr, err := user.LookupId(strconv.Itoa(int(uid))) - if err != nil { - return "" - } - return usr.Username -} - -func GetGroup(gid uint32) string { - usr, err := user.LookupGroupId(strconv.Itoa(int(gid))) - if err != nil { - return "" - } - return usr.Name -} - -const dotCharacter = 46 - -func IsHidden(path string) bool { - return path[0] == dotCharacter -} - -func countLines(path string) (int, error) { - file, err := os.Open(path) - if err != nil { - return 0, err - } - defer file.Close() - - scanner := bufio.NewScanner(file) - lineCount := 0 - for scanner.Scan() { - lineCount++ - } - if err := scanner.Err(); err != nil { - return 0, err - } - return lineCount, nil -} - -func ReadFileByLine(filename string, page, pageSize int, latest bool) (lines []string, isEndOfFile bool, total int, err error) { - if !NewFileOp().Stat(filename) { - return - } - file, err := os.Open(filename) - if err != nil { - return - } - defer file.Close() - - totalLines, err := countLines(filename) - if err != nil { - return - } - total = (totalLines + pageSize - 1) / pageSize - reader := bufio.NewReaderSize(file, 8192) - - if latest { - page = total - } - currentLine := 0 - startLine := (page - 1) * pageSize - endLine := startLine + pageSize - - for { - line, _, err := reader.ReadLine() - if err == io.EOF { - break - } - if currentLine >= startLine && currentLine < endLine { - lines = append(lines, string(line)) - } - currentLine++ - if currentLine >= endLine { - break - } - } - - isEndOfFile = currentLine < endLine - return -} - -func GetParentMode(path string) (os.FileMode, error) { - absPath, err := filepath.Abs(path) - if err != nil { - return 0, err - } - - for { - fileInfo, err := os.Stat(absPath) - if err == nil { - return fileInfo.Mode() & os.ModePerm, nil - } - if !os.IsNotExist(err) { - return 0, err - } - - parentDir := filepath.Dir(absPath) - if parentDir == absPath { - return 0, fmt.Errorf("no existing directory found in the path: %s", path) - } - absPath = parentDir - } -} - -func IsInvalidChar(name string) bool { - return strings.Contains(name, "&") -} - -func IsEmptyDir(dir string) bool { - f, err := os.Open(dir) - if err != nil { - return false - } - defer f.Close() - _, err = f.Readdirnames(1) - return err == io.EOF -} diff --git a/backend/utils/files/zip.go b/backend/utils/files/zip.go deleted file mode 100644 index 0d2896811..000000000 --- a/backend/utils/files/zip.go +++ /dev/null @@ -1,52 +0,0 @@ -package files - -import ( - "fmt" - "path" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/common" -) - -type ZipArchiver struct { -} - -func NewZipArchiver() ShellArchiver { - return &ZipArchiver{} -} - -func (z ZipArchiver) Extract(filePath, dstDir string, secret string) error { - if err := checkCmdAvailability("unzip"); err != nil { - return err - } - return cmd.ExecCmd(fmt.Sprintf("unzip -qo %s -d %s", filePath, dstDir)) -} - -func (z ZipArchiver) Compress(sourcePaths []string, dstFile string, _ string) error { - var err error - tmpFile := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("%s%s.zip", common.RandStr(50), time.Now().Format(constant.DateTimeSlimLayout))) - op := NewFileOp() - defer func() { - _ = op.DeleteFile(tmpFile) - if err != nil { - _ = op.DeleteFile(dstFile) - } - }() - baseDir := path.Dir(sourcePaths[0]) - relativePaths := make([]string, len(sourcePaths)) - for i, sp := range sourcePaths { - relativePaths[i] = path.Base(sp) - } - cmdStr := fmt.Sprintf("zip -qr %s %s", tmpFile, strings.Join(relativePaths, " ")) - if err = cmd.ExecCmdWithDir(cmdStr, baseDir); err != nil { - return err - } - if err = op.Mv(tmpFile, dstFile); err != nil { - return err - } - return nil -} diff --git a/backend/utils/firewall/client.go b/backend/utils/firewall/client.go deleted file mode 100644 index de88fca35..000000000 --- a/backend/utils/firewall/client.go +++ /dev/null @@ -1,39 +0,0 @@ -package firewall - -import ( - "os" - - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/firewall/client" -) - -type FirewallClient interface { - Name() string // ufw firewalld - Start() error - Stop() error - Restart() error - Reload() error - Status() (string, error) // running not running - Version() (string, error) - - ListPort() ([]client.FireInfo, error) - ListForward() ([]client.FireInfo, error) - ListAddress() ([]client.FireInfo, error) - - Port(port client.FireInfo, operation string) error - RichRules(rule client.FireInfo, operation string) error - PortForward(info client.Forward, operation string) error - - EnableForward() error -} - -func NewFirewallClient() (FirewallClient, error) { - if _, err := os.Stat("/usr/sbin/firewalld"); err == nil { - return client.NewFirewalld() - } - if _, err := os.Stat("/usr/sbin/ufw"); err == nil { - return client.NewUfw() - } - return nil, buserr.New(constant.ErrFirewall) -} diff --git a/backend/utils/firewall/client/firewalld.go b/backend/utils/firewall/client/firewalld.go deleted file mode 100644 index e0cd33e24..000000000 --- a/backend/utils/firewall/client/firewalld.go +++ /dev/null @@ -1,263 +0,0 @@ -package client - -import ( - "fmt" - "regexp" - "strings" - "sync" - - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" -) - -var ForwardListRegex = regexp.MustCompile(`^port=(\d{1,5}):proto=(.+?):toport=(\d{1,5}):toaddr=(.*)$`) - -type Firewall struct{} - -func NewFirewalld() (*Firewall, error) { - return &Firewall{}, nil -} - -func (f *Firewall) Name() string { - return "firewalld" -} - -func (f *Firewall) Status() (string, error) { - stdout, _ := cmd.Exec("firewall-cmd --state") - if stdout == "running\n" { - return "running", nil - } - return "not running", nil -} - -func (f *Firewall) Version() (string, error) { - stdout, err := cmd.Exec("firewall-cmd --version") - if err != nil { - return "", fmt.Errorf("load the firewall version failed, err: %s", stdout) - } - return strings.ReplaceAll(stdout, "\n ", ""), nil -} - -func (f *Firewall) Start() error { - stdout, err := cmd.Exec("systemctl start firewalld") - if err != nil { - return fmt.Errorf("enable the firewall failed, err: %s", stdout) - } - return nil -} - -func (f *Firewall) Stop() error { - stdout, err := cmd.Exec("systemctl stop firewalld") - if err != nil { - return fmt.Errorf("stop the firewall failed, err: %s", stdout) - } - return nil -} - -func (f *Firewall) Restart() error { - stdout, err := cmd.Exec("systemctl restart firewalld") - if err != nil { - return fmt.Errorf("restart the firewall failed, err: %s", stdout) - } - return nil -} - -func (f *Firewall) Reload() error { - stdout, err := cmd.Exec("firewall-cmd --reload") - if err != nil { - return fmt.Errorf("reload firewall failed, err: %s", stdout) - } - return nil -} - -func (f *Firewall) ListPort() ([]FireInfo, error) { - var wg sync.WaitGroup - var datas []FireInfo - wg.Add(2) - go func() { - defer wg.Done() - stdout, err := cmd.Exec("firewall-cmd --zone=public --list-ports") - if err != nil { - return - } - ports := strings.Split(strings.ReplaceAll(stdout, "\n", ""), " ") - for _, port := range ports { - if len(port) == 0 { - continue - } - var itemPort FireInfo - if strings.Contains(port, "/") { - itemPort.Port = strings.Split(port, "/")[0] - itemPort.Protocol = strings.Split(port, "/")[1] - } - itemPort.Strategy = "accept" - datas = append(datas, itemPort) - } - }() - - go func() { - defer wg.Done() - stdout1, err := cmd.Exec("firewall-cmd --zone=public --list-rich-rules") - if err != nil { - return - } - rules := strings.Split(stdout1, "\n") - for _, rule := range rules { - if len(rule) == 0 { - continue - } - itemRule := f.loadInfo(rule) - if len(itemRule.Port) != 0 && (itemRule.Family == "ipv4" || (itemRule.Family == "ipv6" && len(itemRule.Address) != 0)) { - datas = append(datas, itemRule) - } - } - }() - wg.Wait() - return datas, nil -} - -func (f *Firewall) ListForward() ([]FireInfo, error) { - stdout, err := cmd.Exec("firewall-cmd --zone=public --list-forward-ports") - if err != nil { - return nil, err - } - var datas []FireInfo - for _, line := range strings.Split(stdout, "\n") { - line = strings.TrimFunc(line, func(r rune) bool { - return r <= 32 - }) - if ForwardListRegex.MatchString(line) { - match := ForwardListRegex.FindStringSubmatch(line) - if len(match) < 4 { - continue - } - if len(match[4]) == 0 { - match[4] = "127.0.0.1" - } - datas = append(datas, FireInfo{ - Port: match[1], - Protocol: match[2], - TargetIP: match[4], - TargetPort: match[3], - }) - } - } - return datas, nil -} - -func (f *Firewall) ListAddress() ([]FireInfo, error) { - stdout, err := cmd.Exec("firewall-cmd --zone=public --list-rich-rules") - if err != nil { - return nil, err - } - var datas []FireInfo - rules := strings.Split(stdout, "\n") - for _, rule := range rules { - if len(rule) == 0 { - continue - } - itemRule := f.loadInfo(rule) - if len(itemRule.Port) == 0 && len(itemRule.Address) != 0 { - datas = append(datas, itemRule) - } - } - return datas, nil -} - -func (f *Firewall) Port(port FireInfo, operation string) error { - if cmd.CheckIllegal(operation, port.Protocol, port.Port) { - return buserr.New(constant.ErrCmdIllegal) - } - - stdout, err := cmd.Execf("firewall-cmd --zone=public --%s-port=%s/%s --permanent", operation, port.Port, port.Protocol) - if err != nil { - return fmt.Errorf("%s (port: %s/%s strategy: %s) failed, err: %s", operation, port.Port, port.Protocol, port.Strategy, stdout) - } - return nil -} - -func (f *Firewall) RichRules(rule FireInfo, operation string) error { - if cmd.CheckIllegal(operation, rule.Address, rule.Protocol, rule.Port, rule.Strategy) { - return buserr.New(constant.ErrCmdIllegal) - } - ruleStr := "rule family=ipv4 " - if strings.Contains(rule.Address, ":") { - ruleStr = "rule family=ipv6 " - } - if len(rule.Address) != 0 { - ruleStr += fmt.Sprintf("source address=%s ", rule.Address) - } - if len(rule.Port) != 0 { - ruleStr += fmt.Sprintf("port port=%s ", rule.Port) - } - if len(rule.Protocol) != 0 { - ruleStr += fmt.Sprintf("protocol=%s ", rule.Protocol) - } - ruleStr += rule.Strategy - stdout, err := cmd.Execf("firewall-cmd --zone=public --%s-rich-rule '%s' --permanent", operation, ruleStr) - if err != nil { - return fmt.Errorf("%s rich rules (%s) failed, err: %s", operation, ruleStr, stdout) - } - if len(rule.Address) == 0 { - stdout1, err := cmd.Execf("firewall-cmd --zone=public --%s-rich-rule '%s' --permanent", operation, strings.ReplaceAll(ruleStr, "family=ipv4 ", "family=ipv6 ")) - if err != nil { - return fmt.Errorf("%s rich rules (%s) failed, err: %s", operation, strings.ReplaceAll(ruleStr, "family=ipv4 ", "family=ipv6 "), stdout1) - } - } - return nil -} - -func (f *Firewall) PortForward(info Forward, operation string) error { - ruleStr := fmt.Sprintf("firewall-cmd --zone=public --%s-forward-port=port=%s:proto=%s:toport=%s --permanent", operation, info.Port, info.Protocol, info.TargetPort) - if info.TargetIP != "" && info.TargetIP != "127.0.0.1" && info.TargetIP != "localhost" { - ruleStr = fmt.Sprintf("firewall-cmd --zone=public --%s-forward-port=port=%s:proto=%s:toaddr=%s:toport=%s --permanent", operation, info.Port, info.Protocol, info.TargetIP, info.TargetPort) - } - - stdout, err := cmd.Exec(ruleStr) - if err != nil { - return fmt.Errorf("%s port forward failed, err: %s", operation, stdout) - } - if err = f.Reload(); err != nil { - return err - } - return nil -} - -func (f *Firewall) loadInfo(line string) FireInfo { - var itemRule FireInfo - ruleInfo := strings.Split(strings.ReplaceAll(line, "\"", ""), " ") - for _, item := range ruleInfo { - switch { - case strings.Contains(item, "family="): - itemRule.Family = strings.ReplaceAll(item, "family=", "") - case strings.Contains(item, "ipset="): - itemRule.Address = strings.ReplaceAll(item, "ipset=", "") - case strings.Contains(item, "address="): - itemRule.Address = strings.ReplaceAll(item, "address=", "") - case strings.Contains(item, "port="): - itemRule.Port = strings.ReplaceAll(item, "port=", "") - case strings.Contains(item, "protocol="): - itemRule.Protocol = strings.ReplaceAll(item, "protocol=", "") - case item == "accept" || item == "drop" || item == "reject": - itemRule.Strategy = item - } - } - return itemRule -} - -func (f *Firewall) EnableForward() error { - stdout, err := cmd.Exec("firewall-cmd --zone=public --query-masquerade") - if err != nil { - if strings.HasSuffix(strings.TrimSpace(stdout), "no") { - stdout, err = cmd.Exec("firewall-cmd --zone=public --add-masquerade --permanent") - if err != nil { - return fmt.Errorf("%s: %s", err, stdout) - } - return f.Reload() - } - return fmt.Errorf("%s: %s", err, stdout) - } - - return nil -} diff --git a/backend/utils/firewall/client/info.go b/backend/utils/firewall/client/info.go deleted file mode 100644 index 3503c25ed..000000000 --- a/backend/utils/firewall/client/info.go +++ /dev/null @@ -1,35 +0,0 @@ -package client - -type FireInfo struct { - Family string `json:"family"` // ipv4 ipv6 - Address string `json:"address"` // Anywhere - Port string `json:"port"` - Protocol string `json:"protocol"` // tcp udp tcp/udp - Strategy string `json:"strategy"` // accept drop - - Num string `json:"num"` - TargetIP string `json:"targetIP"` - TargetPort string `json:"targetPort"` - - UsedStatus string `json:"usedStatus"` - Description string `json:"description"` -} - -type Forward struct { - Num string `json:"num"` - Protocol string `json:"protocol"` - Port string `json:"port"` - TargetIP string `json:"targetIP"` - TargetPort string `json:"targetPort"` -} - -type IptablesNatInfo struct { - Num string `json:"num"` - Target string `json:"target"` - Protocol string `json:"protocol"` - Opt string `json:"opt"` - Source string `json:"source"` - Destination string `json:"destination"` - SrcPort string `json:"srcPort"` - DestPort string `json:"destPort"` -} diff --git a/backend/utils/firewall/client/iptables.go b/backend/utils/firewall/client/iptables.go deleted file mode 100644 index 8f8bf263a..000000000 --- a/backend/utils/firewall/client/iptables.go +++ /dev/null @@ -1,145 +0,0 @@ -package client - -import ( - "fmt" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "regexp" - "strings" -) - -const NatChain = "1PANEL" - -var NatListRegex = regexp.MustCompile(`^(\d+)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)(?:\s+(.+?) .+?:(\d{1,5}(?::\d+)?).+?[ :](.+-.+|(?:.+:)?\d{1,5}(?:-\d{1,5})?))?$`) - -type Iptables struct { - CmdStr string -} - -func NewIptables() (*Iptables, error) { - iptables := new(Iptables) - if cmd.HasNoPasswordSudo() { - iptables.CmdStr = "sudo" - } - - return iptables, nil -} - -func (iptables *Iptables) runf(rule string, a ...any) error { - stdout, err := cmd.Execf("%s iptables -t nat %s", iptables.CmdStr, fmt.Sprintf(rule, a...)) - if err != nil { - return fmt.Errorf("%s, %s", err, stdout) - } - if stdout != "" { - return fmt.Errorf("iptables error: %s", stdout) - } - - return nil -} - -func (iptables *Iptables) Check() error { - stdout, err := cmd.Exec("cat /proc/sys/net/ipv4/ip_forward") - if err != nil { - return fmt.Errorf("%s, %s", err, stdout) - } - if stdout == "0" { - return fmt.Errorf("disable") - } - - return nil -} - -func (iptables *Iptables) NatNewChain() error { - return iptables.runf("-N %s", NatChain) -} - -func (iptables *Iptables) NatAppendChain() error { - return iptables.runf("-A PREROUTING -j %s", NatChain) -} - -func (iptables *Iptables) NatList(chain ...string) ([]IptablesNatInfo, error) { - rule := fmt.Sprintf("%s iptables -t nat -nL %s --line", iptables.CmdStr, NatChain) - if len(chain) == 1 { - rule = fmt.Sprintf("%s iptables -t nat -nL %s --line", iptables.CmdStr, chain[0]) - } - stdout, err := cmd.Exec(rule) - if err != nil { - return nil, err - } - - var forwardList []IptablesNatInfo - for _, line := range strings.Split(stdout, "\n") { - line = strings.TrimFunc(line, func(r rune) bool { - return r <= 32 - }) - if NatListRegex.MatchString(line) { - match := NatListRegex.FindStringSubmatch(line) - if !strings.Contains(match[9], ":") { - match[9] = fmt.Sprintf(":%s", match[9]) - } - forwardList = append(forwardList, IptablesNatInfo{ - Num: match[1], - Target: match[2], - Protocol: match[7], - Opt: match[4], - Source: match[5], - Destination: match[6], - SrcPort: match[8], - DestPort: match[9], - }) - } - } - - return forwardList, nil -} - -func (iptables *Iptables) NatAdd(protocol, src, destIp, destPort string, save bool) error { - rule := fmt.Sprintf("-A %s -p %s --dport %s -j REDIRECT --to-port %s", NatChain, protocol, src, destPort) - if destIp != "" && destIp != "127.0.0.1" && destIp != "localhost" { - rule = fmt.Sprintf("-A %s -p %s --dport %s -j DNAT --to-destination %s:%s", NatChain, protocol, src, destIp, destPort) - } - if err := iptables.runf(rule); err != nil { - return err - } - - if save { - return global.DB.Save(&model.Forward{ - Protocol: protocol, - Port: src, - TargetIP: destIp, - TargetPort: destPort, - }).Error - } - return nil -} - -func (iptables *Iptables) NatRemove(num string, protocol, src, destIp, destPort string) error { - if err := iptables.runf("-D %s %s", NatChain, num); err != nil { - return err - } - - global.DB.Where( - "protocol = ? AND port = ? AND target_ip = ? AND target_port = ?", - protocol, - src, - destIp, - destPort, - ).Delete(&model.Forward{}) - return nil -} - -func (iptables *Iptables) Reload() error { - if err := iptables.runf("-F %s", NatChain); err != nil { - return err - } - - var rules []model.Forward - global.DB.Find(&rules) - for _, forward := range rules { - if err := iptables.NatAdd(forward.Protocol, forward.Port, forward.TargetIP, forward.TargetPort, false); err != nil { - return err - } - } - return nil -} diff --git a/backend/utils/firewall/client/ufw.go b/backend/utils/firewall/client/ufw.go deleted file mode 100644 index a43433263..000000000 --- a/backend/utils/firewall/client/ufw.go +++ /dev/null @@ -1,322 +0,0 @@ -package client - -import ( - "fmt" - "strings" - - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" -) - -type Ufw struct { - CmdStr string -} - -func NewUfw() (*Ufw, error) { - var ufw Ufw - if cmd.HasNoPasswordSudo() { - ufw.CmdStr = "sudo ufw" - } else { - ufw.CmdStr = "ufw" - } - return &ufw, nil -} - -func (f *Ufw) Name() string { - return "ufw" -} - -func (f *Ufw) Status() (string, error) { - stdout, _ := cmd.Execf("%s status | grep Status", f.CmdStr) - if stdout == "Status: active\n" { - return "running", nil - } - stdout1, _ := cmd.Execf("%s status | grep 状态", f.CmdStr) - if stdout1 == "状态: 激活\n" { - return "running", nil - } - return "not running", nil -} - -func (f *Ufw) Version() (string, error) { - stdout, err := cmd.Execf("%s version | grep ufw", f.CmdStr) - if err != nil { - return "", fmt.Errorf("load the firewall status failed, err: %s", stdout) - } - info := strings.ReplaceAll(stdout, "\n", "") - return strings.ReplaceAll(info, "ufw ", ""), nil -} - -func (f *Ufw) Start() error { - stdout, err := cmd.Execf("echo y | %s enable", f.CmdStr) - if err != nil { - return fmt.Errorf("enable the firewall failed, err: %s", stdout) - } - return nil -} - -func (f *Ufw) Stop() error { - stdout, err := cmd.Execf("%s disable", f.CmdStr) - if err != nil { - return fmt.Errorf("stop the firewall failed, err: %s", stdout) - } - return nil -} - -func (f *Ufw) Restart() error { - if err := f.Stop(); err != nil { - return err - } - if err := f.Start(); err != nil { - return err - } - return nil -} - -func (f *Ufw) Reload() error { - return nil -} - -func (f *Ufw) ListPort() ([]FireInfo, error) { - stdout, err := cmd.Execf("%s status verbose", f.CmdStr) - if err != nil { - return nil, err - } - portInfos := strings.Split(stdout, "\n") - var datas []FireInfo - isStart := false - for _, line := range portInfos { - if strings.HasPrefix(line, "-") { - isStart = true - continue - } - if !isStart { - continue - } - itemFire := f.loadInfo(line, "port") - if len(itemFire.Port) != 0 && itemFire.Port != "Anywhere" && !strings.Contains(itemFire.Port, ".") { - itemFire.Port = strings.ReplaceAll(itemFire.Port, ":", "-") - datas = append(datas, itemFire) - } - } - return datas, nil -} - -func (f *Ufw) ListForward() ([]FireInfo, error) { - iptables, err := NewIptables() - if err != nil { - return nil, err - } - rules, err := iptables.NatList() - if err != nil { - return nil, err - } - - var list []FireInfo - for _, rule := range rules { - dest := strings.Split(rule.DestPort, ":") - if len(dest) < 2 { - continue - } - if len(dest[0]) == 0 { - dest[0] = "127.0.0.1" - } - list = append(list, FireInfo{ - Num: rule.Num, - Protocol: rule.Protocol, - Port: rule.SrcPort, - TargetIP: dest[0], - TargetPort: dest[1], - }) - } - return list, nil -} - -func (f *Ufw) ListAddress() ([]FireInfo, error) { - stdout, err := cmd.Execf("%s status verbose", f.CmdStr) - if err != nil { - return nil, err - } - portInfos := strings.Split(stdout, "\n") - var datas []FireInfo - isStart := false - for _, line := range portInfos { - if strings.HasPrefix(line, "-") { - isStart = true - continue - } - if !isStart { - continue - } - if !strings.Contains(line, " IN") { - continue - } - itemFire := f.loadInfo(line, "address") - if strings.Contains(itemFire.Port, ".") { - itemFire.Address += ("-" + itemFire.Port) - itemFire.Port = "" - } - if len(itemFire.Port) == 0 && len(itemFire.Address) != 0 { - datas = append(datas, itemFire) - } - } - return datas, nil -} - -func (f *Ufw) Port(port FireInfo, operation string) error { - switch port.Strategy { - case "accept": - port.Strategy = "allow" - case "drop": - port.Strategy = "deny" - default: - return fmt.Errorf("unsupported strategy %s", port.Strategy) - } - if cmd.CheckIllegal(port.Protocol, port.Port) { - return buserr.New(constant.ErrCmdIllegal) - } - - command := fmt.Sprintf("%s %s %s", f.CmdStr, port.Strategy, port.Port) - if operation == "remove" { - command = fmt.Sprintf("%s delete %s %s", f.CmdStr, port.Strategy, port.Port) - } - if len(port.Protocol) != 0 { - command += fmt.Sprintf("/%s", port.Protocol) - } - stdout, err := cmd.Exec(command) - if err != nil { - return fmt.Errorf("%s (%s) failed, err: %s", operation, command, stdout) - } - return nil -} - -func (f *Ufw) RichRules(rule FireInfo, operation string) error { - switch rule.Strategy { - case "accept": - rule.Strategy = "allow" - case "drop": - rule.Strategy = "deny" - default: - return fmt.Errorf("unsupported strategy %s", rule.Strategy) - } - - if cmd.CheckIllegal(operation, rule.Protocol, rule.Address, rule.Port) { - return buserr.New(constant.ErrCmdIllegal) - } - - ruleStr := fmt.Sprintf("%s insert 1 %s ", f.CmdStr, rule.Strategy) - if operation == "remove" { - ruleStr = fmt.Sprintf("%s delete %s ", f.CmdStr, rule.Strategy) - } - if len(rule.Protocol) != 0 { - ruleStr += fmt.Sprintf("proto %s ", rule.Protocol) - } - if strings.Contains(rule.Address, "-") { - ruleStr += fmt.Sprintf("from %s to %s ", strings.Split(rule.Address, "-")[0], strings.Split(rule.Address, "-")[1]) - } else { - ruleStr += fmt.Sprintf("from %s ", rule.Address) - } - if len(rule.Port) != 0 { - ruleStr += fmt.Sprintf("to any port %s ", rule.Port) - } - - stdout, err := cmd.Exec(ruleStr) - if err != nil { - if strings.Contains(stdout, "ERROR: Invalid position") || strings.Contains(stdout, "ERROR: 无效位置") { - stdout, err := cmd.Exec(strings.ReplaceAll(ruleStr, "insert 1 ", "")) - if err != nil { - return fmt.Errorf("%s rich rules (%s), failed, err: %s", operation, ruleStr, stdout) - } - return nil - } - return fmt.Errorf("%s rich rules (%s), failed, err: %s", operation, ruleStr, stdout) - } - return nil -} - -func (f *Ufw) PortForward(info Forward, operation string) error { - iptables, err := NewIptables() - if err != nil { - return err - } - - if operation == "add" { - err = iptables.NatAdd(info.Protocol, info.Port, info.TargetIP, info.TargetPort, true) - } else { - err = iptables.NatRemove(info.Num, info.Protocol, info.Port, info.TargetIP, info.TargetPort) - } - if err != nil { - return fmt.Errorf("%s port forward failed, err: %s", operation, err) - } - return nil -} - -func (f *Ufw) loadInfo(line string, fireType string) FireInfo { - fields := strings.Fields(line) - var itemInfo FireInfo - if strings.Contains(line, "LIMIT") || strings.Contains(line, "ALLOW FWD") { - return itemInfo - } - if len(fields) < 4 { - return itemInfo - } - if fields[1] == "(v6)" && fireType == "port" { - return itemInfo - } - if fields[0] == "Anywhere" && fireType != "port" { - itemInfo.Strategy = "drop" - if fields[1] == "ALLOW" { - itemInfo.Strategy = "accept" - } - if fields[1] == "(v6)" { - if fields[2] == "ALLOW" { - itemInfo.Strategy = "accept" - } - itemInfo.Address = fields[4] - } else { - itemInfo.Address = fields[3] - } - return itemInfo - } - if strings.Contains(fields[0], "/") { - itemInfo.Port = strings.Split(fields[0], "/")[0] - itemInfo.Protocol = strings.Split(fields[0], "/")[1] - } else { - itemInfo.Port = fields[0] - itemInfo.Protocol = "tcp/udp" - } - itemInfo.Family = "ipv4" - if fields[1] == "ALLOW" { - itemInfo.Strategy = "accept" - } else { - itemInfo.Strategy = "drop" - } - itemInfo.Address = fields[3] - - return itemInfo -} - -func (f *Ufw) EnableForward() error { - iptables, err := NewIptables() - if err != nil { - return err - } - _ = iptables.NatNewChain() - - rules, err := iptables.NatList("PREROUTING") - if err != nil { - return err - } - for _, rule := range rules { - if rule.Target == NatChain { - goto reload - } - } - - if err = iptables.NatAppendChain(); err != nil { - return err - } -reload: - return iptables.Reload() -} diff --git a/backend/utils/http/get.go b/backend/utils/http/get.go deleted file mode 100644 index 4dd90da55..000000000 --- a/backend/utils/http/get.go +++ /dev/null @@ -1,42 +0,0 @@ -package http - -import ( - "context" - "errors" - "net/http" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/utils/xpack" -) - -func GetHttpRes(url string) (*http.Response, error) { - client := &http.Client{ - Timeout: time.Second * 300, - } - transport := xpack.LoadRequestTransport() - client.Transport = transport - - req, err := http.NewRequestWithContext(context.Background(), "GET", url, nil) - if err != nil { - return nil, buserr.WithMap("ErrCreateHttpClient", map[string]interface{}{"err": err.Error()}, err) - } - - resp, err := client.Do(req) - if err != nil { - if errors.Is(err, context.DeadlineExceeded) { - return nil, buserr.WithMap("ErrHttpReqTimeOut", map[string]interface{}{"err": err.Error()}, err) - } else { - if strings.Contains(err.Error(), "no such host") { - return nil, buserr.New("ErrNoSuchHost") - } - return nil, buserr.WithMap("ErrHttpReqFailed", map[string]interface{}{"err": err.Error()}, err) - } - } - if resp.StatusCode == 404 { - return nil, buserr.New("ErrHttpReqNotFound") - } - - return resp, nil -} diff --git a/backend/utils/http/request.go b/backend/utils/http/request.go deleted file mode 100644 index 6445ec8ef..000000000 --- a/backend/utils/http/request.go +++ /dev/null @@ -1,45 +0,0 @@ -package http - -import ( - "context" - "io" - "net/http" - "time" - - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/xpack" -) - -func HandleGet(url, method string, timeout int) (int, []byte, error) { - transport := xpack.LoadRequestTransport() - return HandleGetWithTransport(url, method, transport, timeout) -} - -func HandleGetWithTransport(url, method string, transport *http.Transport, timeout int) (int, []byte, error) { - defer func() { - if r := recover(); r != nil { - global.LOG.Errorf("handle request failed, error message: %v", r) - return - } - }() - - client := http.Client{Timeout: time.Duration(timeout) * time.Second, Transport: transport} - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) - defer cancel() - request, err := http.NewRequestWithContext(ctx, method, url, nil) - if err != nil { - return 0, nil, err - } - request.Header.Set("Content-Type", "application/json") - resp, err := client.Do(request) - if err != nil { - return 0, nil, err - } - body, err := io.ReadAll(resp.Body) - if err != nil { - return 0, nil, err - } - defer resp.Body.Close() - - return resp.StatusCode, body, nil -} diff --git a/backend/utils/ini_conf/ini.go b/backend/utils/ini_conf/ini.go deleted file mode 100644 index 4ea80a2a6..000000000 --- a/backend/utils/ini_conf/ini.go +++ /dev/null @@ -1,19 +0,0 @@ -package ini_conf - -import "gopkg.in/ini.v1" - -func GetIniValue(filePath, Group, Key string) (string, error) { - cfg, err := ini.Load(filePath) - if err != nil { - return "", err - } - service, err := cfg.GetSection(Group) - if err != nil { - return "", err - } - startKey, err := service.GetKey(Key) - if err != nil { - return "", err - } - return startKey.Value(), nil -} diff --git a/backend/utils/jwt/jwt.go b/backend/utils/jwt/jwt.go deleted file mode 100644 index f8c1c1529..000000000 --- a/backend/utils/jwt/jwt.go +++ /dev/null @@ -1,69 +0,0 @@ -package jwt - -import ( - "time" - - "github.com/1Panel-dev/1Panel/backend/app/repo" - "github.com/1Panel-dev/1Panel/backend/constant" - - "github.com/golang-jwt/jwt/v4" -) - -type JWT struct { - SigningKey []byte -} - -type JwtRequest struct { - BaseClaims - BufferTime int64 - jwt.RegisteredClaims -} - -type CustomClaims struct { - BaseClaims - BufferTime int64 - jwt.RegisteredClaims -} - -type BaseClaims struct { - ID uint - Name string -} - -func NewJWT() *JWT { - settingRepo := repo.NewISettingRepo() - jwtSign, _ := settingRepo.Get(settingRepo.WithByKey("JWTSigningKey")) - return &JWT{ - []byte(jwtSign.Value), - } -} - -func (j *JWT) CreateClaims(baseClaims BaseClaims) CustomClaims { - claims := CustomClaims{ - BaseClaims: baseClaims, - BufferTime: constant.JWTBufferTime, - RegisteredClaims: jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * time.Duration(constant.JWTBufferTime))), - Issuer: constant.JWTIssuer, - }, - } - return claims -} - -func (j *JWT) CreateToken(request CustomClaims) (string, error) { - token := jwt.NewWithClaims(jwt.SigningMethodHS256, &request) - return token.SignedString(j.SigningKey) -} - -func (j *JWT) ParseToken(tokenStr string) (*JwtRequest, error) { - token, err := jwt.ParseWithClaims(tokenStr, &JwtRequest{}, func(token *jwt.Token) (interface{}, error) { - return j.SigningKey, nil - }) - if err != nil || token == nil { - return nil, constant.ErrTokenParse - } - if claims, ok := token.Claims.(*JwtRequest); ok && token.Valid { - return claims, nil - } - return nil, constant.ErrTokenParse -} diff --git a/backend/utils/mysql/client.go b/backend/utils/mysql/client.go deleted file mode 100644 index 364d0ea3e..000000000 --- a/backend/utils/mysql/client.go +++ /dev/null @@ -1,77 +0,0 @@ -package mysql - -import ( - "context" - "database/sql" - "errors" - "fmt" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/mysql/client" -) - -type MysqlClient interface { - Create(info client.CreateInfo) error - CreateUser(info client.CreateInfo, withDeleteDB bool) error - Delete(info client.DeleteInfo) error - - ChangePassword(info client.PasswordChangeInfo) error - ChangeAccess(info client.AccessChangeInfo) error - - Backup(info client.BackupInfo) error - Recover(info client.RecoverInfo) error - - SyncDB(version string) ([]client.SyncDBInfo, error) - Close() -} - -func NewMysqlClient(conn client.DBInfo) (MysqlClient, error) { - if conn.From == "local" { - connArgs := []string{"exec", conn.Address, conn.Type, "-u" + conn.Username, "-p" + conn.Password, "-e"} - return client.NewLocal(connArgs, conn.Type, conn.Address, conn.Password, conn.Database), nil - } - - if strings.Contains(conn.Address, ":") { - conn.Address = fmt.Sprintf("[%s]", conn.Address) - } - - tlsItem, err := client.ConnWithSSL(conn.SSL, conn.SkipVerify, conn.ClientKey, conn.ClientCert, conn.RootCert) - if err != nil { - return nil, err - } - connArgs := fmt.Sprintf("%s:%s@tcp(%s:%d)/?charset=utf8%s", conn.Username, conn.Password, conn.Address, conn.Port, tlsItem) - db, err := sql.Open("mysql", connArgs) - if err != nil { - return nil, err - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(conn.Timeout)*time.Second) - defer cancel() - if err := db.PingContext(ctx); err != nil { - global.LOG.Errorf("test mysql conn failed, err: %v", err) - return nil, err - } - if errors.Is(ctx.Err(), context.DeadlineExceeded) { - return nil, buserr.New(constant.ErrExecTimeOut) - } - - return client.NewRemote(client.Remote{ - Type: conn.Type, - Client: db, - Database: conn.Database, - User: conn.Username, - Password: conn.Password, - Address: conn.Address, - Port: conn.Port, - - SSL: conn.SSL, - RootCert: conn.RootCert, - ClientKey: conn.ClientKey, - ClientCert: conn.ClientCert, - SkipVerify: conn.SkipVerify, - }), nil -} diff --git a/backend/utils/mysql/client/info.go b/backend/utils/mysql/client/info.go deleted file mode 100644 index 3c6c4c25b..000000000 --- a/backend/utils/mysql/client/info.go +++ /dev/null @@ -1,137 +0,0 @@ -package client - -import ( - "crypto/tls" - "crypto/x509" - "errors" - - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/go-sql-driver/mysql" -) - -type DBInfo struct { - Type string `json:"type"` - From string `json:"from"` - Database string `json:"database"` - Address string `json:"address"` - Port uint `json:"port"` - Username string `json:"userName"` - Password string `json:"password"` - - SSL bool `json:"ssl"` - RootCert string `json:"rootCert"` - ClientKey string `json:"clientKey"` - ClientCert string `json:"clientCert"` - SkipVerify bool `json:"skipVerify"` - - Timeout uint `json:"timeout"` // second -} - -type CreateInfo struct { - Name string `json:"name"` - Format string `json:"format"` - Version string `json:"version"` - Username string `json:"userName"` - Password string `json:"password"` - Permission string `json:"permission"` - - Timeout uint `json:"timeout"` // second -} - -type DeleteInfo struct { - Name string `json:"name"` - Version string `json:"version"` - Username string `json:"userName"` - Permission string `json:"permission"` - - ForceDelete bool `json:"forceDelete"` - Timeout uint `json:"timeout"` // second -} - -type PasswordChangeInfo struct { - Name string `json:"name"` - Version string `json:"version"` - Username string `json:"userName"` - Password string `json:"password"` - Permission string `json:"permission"` - - Timeout uint `json:"timeout"` // second -} - -type AccessChangeInfo struct { - Name string `json:"name"` - Version string `json:"version"` - Username string `json:"userName"` - Password string `json:"password"` - OldPermission string `json:"oldPermission"` - Permission string `json:"permission"` - - Timeout uint `json:"timeout"` // second -} - -type BackupInfo struct { - Name string `json:"name"` - Type string `json:"type"` - Version string `json:"version"` - Format string `json:"format"` - TargetDir string `json:"targetDir"` - FileName string `json:"fileName"` - - Timeout uint `json:"timeout"` // second -} - -type RecoverInfo struct { - Name string `json:"name"` - Type string `json:"type"` - Version string `json:"version"` - Format string `json:"format"` - SourceFile string `json:"sourceFile"` - - Timeout uint `json:"timeout"` // second -} - -type SyncDBInfo struct { - Name string `json:"name"` - From string `json:"from"` - MysqlName string `json:"mysqlName"` - Format string `json:"format"` - Username string `json:"username"` - Password string `json:"password"` - Permission string `json:"permission"` -} - -var formatMap = map[string]string{ - "utf8": "utf8_general_ci", - "utf8mb4": "utf8mb4_general_ci", - "gbk": "gbk_chinese_ci", - "big5": "big5_chinese_ci", -} - -func ConnWithSSL(ssl, skipVerify bool, clientKey, clientCert, rootCert string) (string, error) { - if !ssl { - return "", nil - } - tlsConfig := &tls.Config{ - InsecureSkipVerify: skipVerify, - } - if len(rootCert) != 0 { - pool := x509.NewCertPool() - if ok := pool.AppendCertsFromPEM([]byte(rootCert)); !ok { - global.LOG.Error("append certs from pem failed") - return "", errors.New("unable to append root cert to pool") - } - tlsConfig.RootCAs = pool - } - if len(clientCert) != 0 && len(clientKey) != 0 { - cert, err := tls.X509KeyPair([]byte(clientCert), []byte(clientKey)) - if err != nil { - return "", err - } - tlsConfig.Certificates = []tls.Certificate{cert} - } - if err := mysql.RegisterTLSConfig("cloudsql", tlsConfig); err != nil { - global.LOG.Errorf("register tls config failed, err: %v", err) - return "", err - } - return "&tls=cloudsql", nil -} diff --git a/backend/utils/mysql/client/local.go b/backend/utils/mysql/client/local.go deleted file mode 100644 index 3110e3f06..000000000 --- a/backend/utils/mysql/client/local.go +++ /dev/null @@ -1,381 +0,0 @@ -package client - -import ( - "bytes" - "compress/gzip" - "context" - "errors" - "fmt" - "os" - "os/exec" - "path" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/files" -) - -type Local struct { - Type string - PrefixCommand []string - Database string - Password string - ContainerName string -} - -func NewLocal(command []string, dbType, containerName, password, database string) *Local { - return &Local{Type: dbType, PrefixCommand: command, ContainerName: containerName, Password: password, Database: database} -} - -func (r *Local) Create(info CreateInfo) error { - createSql := fmt.Sprintf("create database `%s` default character set %s collate %s", info.Name, info.Format, formatMap[info.Format]) - if err := r.ExecSQL(createSql, info.Timeout); err != nil { - if strings.Contains(strings.ToLower(err.Error()), "error 1007") { - return buserr.New(constant.ErrDatabaseIsExist) - } - return err - } - - if err := r.CreateUser(info, true); err != nil { - _ = r.ExecSQL(fmt.Sprintf("drop database if exists `%s`", info.Name), info.Timeout) - return err - } - - return nil -} - -func (r *Local) CreateUser(info CreateInfo, withDeleteDB bool) error { - var userlist []string - if strings.Contains(info.Permission, ",") { - ips := strings.Split(info.Permission, ",") - for _, ip := range ips { - if len(ip) != 0 { - userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, ip)) - } - } - } else { - userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, info.Permission)) - } - - for _, user := range userlist { - if err := r.ExecSQL(fmt.Sprintf("create user %s identified by '%s';", user, info.Password), info.Timeout); err != nil { - if strings.Contains(strings.ToLower(err.Error()), "error 1396") { - return buserr.New(constant.ErrUserIsExist) - } - if withDeleteDB { - _ = r.Delete(DeleteInfo{ - Name: info.Name, - Version: info.Version, - Username: info.Username, - Permission: info.Permission, - ForceDelete: true, - Timeout: 300}) - } - return err - } - grantStr := fmt.Sprintf("grant all privileges on `%s`.* to %s", info.Name, user) - if info.Name == "*" { - grantStr = fmt.Sprintf("grant all privileges on *.* to %s", user) - } - if strings.HasPrefix(info.Version, "5.7") || strings.HasPrefix(info.Version, "5.6") { - grantStr = fmt.Sprintf("%s identified by '%s' with grant option;", grantStr, info.Password) - } else { - grantStr = grantStr + " with grant option;" - } - if err := r.ExecSQL(grantStr, info.Timeout); err != nil { - if withDeleteDB { - _ = r.Delete(DeleteInfo{ - Name: info.Name, - Version: info.Version, - Username: info.Username, - Permission: info.Permission, - ForceDelete: true, - Timeout: 300}) - } - return err - } - } - return nil -} - -func (r *Local) Delete(info DeleteInfo) error { - var userlist []string - if strings.Contains(info.Permission, ",") { - ips := strings.Split(info.Permission, ",") - for _, ip := range ips { - if len(ip) != 0 { - userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, ip)) - } - } - } else { - userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, info.Permission)) - } - - for _, user := range userlist { - if strings.HasPrefix(info.Version, "5.6") { - if err := r.ExecSQL(fmt.Sprintf("drop user %s", user), info.Timeout); err != nil && !info.ForceDelete { - return err - } - } else { - if err := r.ExecSQL(fmt.Sprintf("drop user if exists %s", user), info.Timeout); err != nil && !info.ForceDelete { - return err - } - } - } - if len(info.Name) != 0 { - if err := r.ExecSQL(fmt.Sprintf("drop database if exists `%s`", info.Name), info.Timeout); err != nil && !info.ForceDelete { - return err - } - } - if !info.ForceDelete { - global.LOG.Info("execute delete database sql successful, now start to drop uploads and records") - } - - return nil -} - -func (r *Local) ChangePassword(info PasswordChangeInfo) error { - if info.Username != "root" { - var userlist []string - if strings.Contains(info.Permission, ",") { - ips := strings.Split(info.Permission, ",") - for _, ip := range ips { - if len(ip) != 0 { - userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, ip)) - } - } - } else { - userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, info.Permission)) - } - - for _, user := range userlist { - passwordChangeSql := fmt.Sprintf("set password for %s = password('%s')", user, info.Password) - if !strings.HasPrefix(info.Version, "5.7") && !strings.HasPrefix(info.Version, "5.6") { - passwordChangeSql = fmt.Sprintf("ALTER USER %s IDENTIFIED BY '%s';", user, info.Password) - } - if err := r.ExecSQL(passwordChangeSql, info.Timeout); err != nil { - return err - } - } - return nil - } - - hosts, err := r.ExecSQLForRows("select host from mysql.user where user='root';", info.Timeout) - if err != nil { - return err - } - for _, host := range hosts { - if host == "%" || host == "localhost" { - passwordRootChangeCMD := fmt.Sprintf("set password for 'root'@'%s' = password('%s')", host, info.Password) - if !strings.HasPrefix(info.Version, "5.7") && !strings.HasPrefix(info.Version, "5.6") { - passwordRootChangeCMD = fmt.Sprintf("alter user 'root'@'%s' identified by '%s';", host, info.Password) - } - if err := r.ExecSQL(passwordRootChangeCMD, info.Timeout); err != nil { - return err - } - } - } - - return nil -} - -func (r *Local) ChangeAccess(info AccessChangeInfo) error { - if info.Username == "root" { - info.OldPermission = "%" - info.Name = "*" - info.Password = r.Password - } - if info.Permission != info.OldPermission { - if err := r.Delete(DeleteInfo{ - Version: info.Version, - Username: info.Username, - Permission: info.OldPermission, - ForceDelete: true, - Timeout: 300}); err != nil { - return err - } - if info.Username == "root" { - return nil - } - } - if err := r.CreateUser(CreateInfo{ - Name: info.Name, - Version: info.Version, - Username: info.Username, - Password: info.Password, - Permission: info.Permission, - Timeout: info.Timeout, - }, false); err != nil { - return err - } - if err := r.ExecSQL("flush privileges", 300); err != nil { - return err - } - return nil -} - -func (r *Local) Backup(info BackupInfo) error { - fileOp := files.NewFileOp() - if !fileOp.Stat(info.TargetDir) { - if err := os.MkdirAll(info.TargetDir, os.ModePerm); err != nil { - return fmt.Errorf("mkdir %s failed, err: %v", info.TargetDir, err) - } - } - outfile, err := os.OpenFile(path.Join(info.TargetDir, info.FileName), os.O_RDWR|os.O_CREATE, 0755) - if err != nil { - return fmt.Errorf("open file %s failed, err: %v", path.Join(info.TargetDir, info.FileName), err) - } - defer outfile.Close() - dumpCmd := "mysqldump" - if r.Type == constant.AppMariaDB { - dumpCmd = "mariadb-dump" - } - global.LOG.Infof("start to %s | gzip > %s.gzip", dumpCmd, info.TargetDir+"/"+info.FileName) - cmd := exec.Command("docker", "exec", r.ContainerName, dumpCmd, "-uroot", "-p"+r.Password, "--default-character-set="+info.Format, info.Name) - var stderr bytes.Buffer - cmd.Stderr = &stderr - - gzipCmd := exec.Command("gzip", "-cf") - gzipCmd.Stdin, _ = cmd.StdoutPipe() - gzipCmd.Stdout = outfile - _ = gzipCmd.Start() - - if err := cmd.Run(); err != nil { - return fmt.Errorf("handle backup database failed, err: %v", stderr.String()) - } - _ = gzipCmd.Wait() - return nil -} - -func (r *Local) Recover(info RecoverInfo) error { - fi, _ := os.Open(info.SourceFile) - defer fi.Close() - cmd := exec.Command("docker", "exec", "-i", r.ContainerName, r.Type, "-uroot", "-p"+r.Password, "--default-character-set="+info.Format, info.Name) - if strings.HasSuffix(info.SourceFile, ".gz") { - gzipFile, err := os.Open(info.SourceFile) - if err != nil { - return err - } - defer gzipFile.Close() - gzipReader, err := gzip.NewReader(gzipFile) - if err != nil { - return err - } - defer gzipReader.Close() - cmd.Stdin = gzipReader - } else { - cmd.Stdin = fi - } - stdout, err := cmd.CombinedOutput() - stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "") - if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") { - return errors.New(stdStr) - } - - return nil -} - -func (r *Local) SyncDB(version string) ([]SyncDBInfo, error) { - var datas []SyncDBInfo - lines, err := r.ExecSQLForRows("SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME FROM information_schema.SCHEMATA", 300) - if err != nil { - return datas, err - } - for _, line := range lines { - parts := strings.Fields(line) - if len(parts) != 2 { - continue - } - if parts[0] == "SCHEMA_NAME" || parts[0] == "information_schema" || parts[0] == "mysql" || parts[0] == "performance_schema" || parts[0] == "sys" || parts[0] == "__recycle_bin__" || parts[0] == "recycle_bin" { - continue - } - dataItem := SyncDBInfo{ - Name: parts[0], - From: "local", - MysqlName: r.Database, - Format: parts[1], - } - userLines, err := r.ExecSQLForRows(fmt.Sprintf("select user,host from mysql.db where db = '%s'", parts[0]), 300) - if err != nil { - global.LOG.Debugf("sync user of db %s failed, err: %v", parts[0], err) - dataItem.Permission = "%" - datas = append(datas, dataItem) - continue - } - - var permissionItem []string - isLocal := true - i := 0 - for _, userline := range userLines { - userparts := strings.Fields(userline) - if len(userparts) != 2 { - continue - } - if userparts[0] == "root" { - continue - } - if i == 0 { - dataItem.Username = userparts[0] - } - dataItem.Username = userparts[0] - if dataItem.Username == userparts[0] && userparts[1] == "%" { - isLocal = false - dataItem.Permission = "%" - } else if dataItem.Username == userparts[0] && userparts[1] != "localhost" { - isLocal = false - permissionItem = append(permissionItem, userparts[1]) - } - } - if len(dataItem.Username) == 0 { - dataItem.Permission = "%" - } else { - if isLocal { - dataItem.Permission = "localhost" - } - if len(dataItem.Permission) == 0 { - dataItem.Permission = strings.Join(permissionItem, ",") - } - } - datas = append(datas, dataItem) - } - return datas, nil -} - -func (r *Local) Close() {} - -func (r *Local) ExecSQL(command string, timeout uint) error { - itemCommand := r.PrefixCommand[:] - itemCommand = append(itemCommand, command) - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) - defer cancel() - cmd := exec.CommandContext(ctx, "docker", itemCommand...) - stdout, err := cmd.CombinedOutput() - if errors.Is(ctx.Err(), context.DeadlineExceeded) { - return buserr.New(constant.ErrExecTimeOut) - } - stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "") - if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") { - return errors.New(stdStr) - } - return nil -} - -func (r *Local) ExecSQLForRows(command string, timeout uint) ([]string, error) { - itemCommand := r.PrefixCommand[:] - itemCommand = append(itemCommand, command) - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) - defer cancel() - cmd := exec.CommandContext(ctx, "docker", itemCommand...) - stdout, err := cmd.CombinedOutput() - if errors.Is(ctx.Err(), context.DeadlineExceeded) { - return nil, buserr.New(constant.ErrExecTimeOut) - } - stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "") - if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") { - return nil, errors.New(stdStr) - } - return strings.Split(stdStr, "\n"), nil -} diff --git a/backend/utils/mysql/client/remote.go b/backend/utils/mysql/client/remote.go deleted file mode 100644 index 9596ba9de..000000000 --- a/backend/utils/mysql/client/remote.go +++ /dev/null @@ -1,472 +0,0 @@ -package client - -import ( - "bytes" - "compress/gzip" - "context" - "database/sql" - "errors" - "fmt" - "os" - "os/exec" - "path" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/files" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/client" -) - -type Remote struct { - Type string - Client *sql.DB - Database string - User string - Password string - Address string - Port uint - - SSL bool - RootCert string - ClientKey string - ClientCert string - SkipVerify bool -} - -func NewRemote(db Remote) *Remote { - return &db -} - -func (r *Remote) Create(info CreateInfo) error { - createSql := fmt.Sprintf("create database `%s` default character set %s collate %s", info.Name, info.Format, formatMap[info.Format]) - if err := r.ExecSQL(createSql, info.Timeout); err != nil { - if strings.Contains(strings.ToLower(err.Error()), "error 1007") { - return buserr.New(constant.ErrDatabaseIsExist) - } - return err - } - - if err := r.CreateUser(info, true); err != nil { - _ = r.ExecSQL(fmt.Sprintf("drop database if exists `%s`", info.Name), info.Timeout) - return err - } - - return nil -} - -func (r *Remote) CreateUser(info CreateInfo, withDeleteDB bool) error { - var userlist []string - if strings.Contains(info.Permission, ",") { - ips := strings.Split(info.Permission, ",") - for _, ip := range ips { - if len(ip) != 0 { - userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, ip)) - } - } - } else { - userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, info.Permission)) - } - - for _, user := range userlist { - if err := r.ExecSQL(fmt.Sprintf("create user %s identified by '%s';", user, info.Password), info.Timeout); err != nil { - if strings.Contains(strings.ToLower(err.Error()), "error 1396") { - return buserr.New(constant.ErrUserIsExist) - } - if withDeleteDB { - _ = r.Delete(DeleteInfo{ - Name: info.Name, - Version: info.Version, - Username: info.Username, - Permission: info.Permission, - ForceDelete: true, - Timeout: 300}) - } - return err - } - grantStr := fmt.Sprintf("grant all privileges on `%s`.* to %s", info.Name, user) - if info.Name == "*" { - grantStr = fmt.Sprintf("grant all privileges on *.* to %s", user) - } - if strings.HasPrefix(info.Version, "5.7") || strings.HasPrefix(info.Version, "5.6") { - grantStr = fmt.Sprintf("%s identified by '%s' with grant option;", grantStr, info.Password) - } else { - grantStr = grantStr + " with grant option;" - } - if err := r.ExecSQL(grantStr, info.Timeout); err != nil { - if withDeleteDB { - _ = r.Delete(DeleteInfo{ - Name: info.Name, - Version: info.Version, - Username: info.Username, - Permission: info.Permission, - ForceDelete: true, - Timeout: 300}) - } - return err - } - } - return nil -} - -func (r *Remote) Delete(info DeleteInfo) error { - var userlist []string - if strings.Contains(info.Permission, ",") { - ips := strings.Split(info.Permission, ",") - for _, ip := range ips { - if len(ip) != 0 { - userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, ip)) - } - } - } else { - userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, info.Permission)) - } - - for _, user := range userlist { - if strings.HasPrefix(info.Version, "5.6") { - if err := r.ExecSQL(fmt.Sprintf("drop user %s", user), info.Timeout); err != nil && !info.ForceDelete { - return err - } - } else { - if err := r.ExecSQL(fmt.Sprintf("drop user if exists %s", user), info.Timeout); err != nil && !info.ForceDelete { - return err - } - } - } - if len(info.Name) != 0 { - if err := r.ExecSQL(fmt.Sprintf("drop database if exists `%s`", info.Name), info.Timeout); err != nil && !info.ForceDelete { - return err - } - } - if !info.ForceDelete { - global.LOG.Info("execute delete database sql successful, now start to drop uploads and records") - } - - return nil -} - -func (r *Remote) ChangePassword(info PasswordChangeInfo) error { - if info.Username != "root" { - var userlist []string - if strings.Contains(info.Permission, ",") { - ips := strings.Split(info.Permission, ",") - for _, ip := range ips { - if len(ip) != 0 { - userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, ip)) - } - } - } else { - userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, info.Permission)) - } - - for _, user := range userlist { - passwordChangeSql := fmt.Sprintf("set password for %s = password('%s')", user, info.Password) - if !strings.HasPrefix(info.Version, "5.7") && !strings.HasPrefix(info.Version, "5.6") { - passwordChangeSql = fmt.Sprintf("ALTER USER %s IDENTIFIED BY '%s';", user, info.Password) - } - if err := r.ExecSQL(passwordChangeSql, info.Timeout); err != nil { - return err - } - } - return nil - } - - hosts, err := r.ExecSQLForHosts(info.Timeout) - if err != nil { - return err - } - for _, host := range hosts { - if host == "%" || host == "localhost" { - passwordRootChangeCMD := fmt.Sprintf("set password for 'root'@'%s' = password('%s')", host, info.Password) - if !strings.HasPrefix(info.Version, "5.7") && !strings.HasPrefix(info.Version, "5.6") { - passwordRootChangeCMD = fmt.Sprintf("alter user 'root'@'%s' identified by '%s';", host, info.Password) - } - if err := r.ExecSQL(passwordRootChangeCMD, info.Timeout); err != nil { - return err - } - } - } - - return nil -} - -func (r *Remote) ChangeAccess(info AccessChangeInfo) error { - if info.Username == "root" { - info.OldPermission = "%" - info.Name = "*" - info.Password = r.Password - } - if info.Permission != info.OldPermission { - if err := r.Delete(DeleteInfo{ - Version: info.Version, - Username: info.Username, - Permission: info.OldPermission, - ForceDelete: true, - Timeout: 300}); err != nil { - return err - } - if info.Username == "root" { - return nil - } - } - if err := r.CreateUser(CreateInfo{ - Name: info.Name, - Version: info.Version, - Username: info.Username, - Password: info.Password, - Permission: info.Permission, - Timeout: info.Timeout, - }, false); err != nil { - return err - } - if err := r.ExecSQL("flush privileges", 300); err != nil { - return err - } - return nil -} - -func (r *Remote) Backup(info BackupInfo) error { - fileOp := files.NewFileOp() - if !fileOp.Stat(info.TargetDir) { - if err := os.MkdirAll(info.TargetDir, os.ModePerm); err != nil { - return fmt.Errorf("mkdir %s failed, err: %v", info.TargetDir, err) - } - } - outfile, err := os.OpenFile(path.Join(info.TargetDir, info.FileName), os.O_RDWR|os.O_CREATE, 0755) - if err != nil { - return fmt.Errorf("open file %s failed, err: %v", path.Join(info.TargetDir, info.FileName), err) - } - defer outfile.Close() - dumpCmd := "mysqldump" - if r.Type == constant.AppMariaDB { - dumpCmd = "mariadb-dump" - } - global.LOG.Infof("start to %s | gzip > %s.gzip", dumpCmd, info.TargetDir+"/"+info.FileName) - image, err := loadImage(info.Type, info.Version) - if err != nil { - return err - } - backupCmd := fmt.Sprintf("docker run --rm --net=host -i %s /bin/bash -c '%s -h %s -P %d -u%s -p%s %s --default-character-set=%s %s'", - image, dumpCmd, r.Address, r.Port, r.User, r.Password, sslSkip(info.Version, r.Type), info.Format, info.Name) - - global.LOG.Debug(strings.ReplaceAll(backupCmd, r.Password, "******")) - cmd := exec.Command("bash", "-c", backupCmd) - var stderr bytes.Buffer - cmd.Stderr = &stderr - - gzipCmd := exec.Command("gzip", "-cf") - gzipCmd.Stdin, _ = cmd.StdoutPipe() - gzipCmd.Stdout = outfile - - _ = gzipCmd.Start() - if err := cmd.Run(); err != nil { - return fmt.Errorf("handle backup database failed, err: %v", stderr.String()) - } - _ = gzipCmd.Wait() - return nil -} - -func (r *Remote) Recover(info RecoverInfo) error { - fi, _ := os.Open(info.SourceFile) - defer fi.Close() - - image, err := loadImage(info.Type, info.Version) - if err != nil { - return err - } - - recoverCmd := fmt.Sprintf("docker run --rm --net=host -i %s /bin/bash -c '%s -h %s -P %d -u%s -p%s %s --default-character-set=%s %s'", - image, r.Type, r.Address, r.Port, r.User, r.Password, sslSkip(info.Version, r.Type), info.Format, info.Name) - - global.LOG.Debug(strings.ReplaceAll(recoverCmd, r.Password, "******")) - cmd := exec.Command("bash", "-c", recoverCmd) - - if strings.HasSuffix(info.SourceFile, ".gz") { - gzipFile, err := os.Open(info.SourceFile) - if err != nil { - return err - } - defer gzipFile.Close() - gzipReader, err := gzip.NewReader(gzipFile) - if err != nil { - return err - } - defer gzipReader.Close() - cmd.Stdin = gzipReader - } else { - cmd.Stdin = fi - } - stdout, err := cmd.CombinedOutput() - stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "") - if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") { - return errors.New(stdStr) - } - - return nil -} - -func (r *Remote) SyncDB(version string) ([]SyncDBInfo, error) { - var datas []SyncDBInfo - rows, err := r.Client.Query("select schema_name, default_character_set_name from information_schema.SCHEMATA") - if err != nil { - return datas, err - } - defer rows.Close() - - for rows.Next() { - var dbName, charsetName string - if err = rows.Scan(&dbName, &charsetName); err != nil { - return datas, err - } - if dbName == "information_schema" || dbName == "mysql" || dbName == "performance_schema" || dbName == "sys" || dbName == "__recycle_bin__" || dbName == "recycle_bin" { - continue - } - dataItem := SyncDBInfo{ - Name: dbName, - From: "remote", - MysqlName: r.Database, - Format: charsetName, - } - userRows, err := r.Client.Query("select user,host from mysql.db where db = ?", dbName) - if err != nil { - global.LOG.Debugf("sync user of db %s failed, err: %v", dbName, err) - dataItem.Permission = "%" - datas = append(datas, dataItem) - continue - } - - var permissionItem []string - isLocal := true - i := 0 - for userRows.Next() { - var user, host string - if err = userRows.Scan(&user, &host); err != nil { - return datas, err - } - if user == "root" { - continue - } - if i == 0 { - dataItem.Username = user - } - if dataItem.Username == user && host == "%" { - isLocal = false - dataItem.Permission = "%" - } else if dataItem.Username == user && host != "localhost" { - isLocal = false - permissionItem = append(permissionItem, host) - } - i++ - } - if len(dataItem.Username) == 0 { - dataItem.Permission = "%" - } else { - if isLocal { - dataItem.Permission = "localhost" - } - if len(dataItem.Permission) == 0 { - dataItem.Permission = strings.Join(permissionItem, ",") - } - } - datas = append(datas, dataItem) - } - if err = rows.Err(); err != nil { - return datas, err - } - return datas, nil -} - -func (r *Remote) Close() { - _ = r.Client.Close() -} - -func (r *Remote) ExecSQL(command string, timeout uint) error { - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) - defer cancel() - - if _, err := r.Client.ExecContext(ctx, command); err != nil { - return err - } - if errors.Is(ctx.Err(), context.DeadlineExceeded) { - return buserr.New(constant.ErrExecTimeOut) - } - - return nil -} - -func (r *Remote) ExecSQLForHosts(timeout uint) ([]string, error) { - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) - defer cancel() - - results, err := r.Client.QueryContext(ctx, "select host from mysql.user where user='root';") - if err != nil { - return nil, err - } - if errors.Is(ctx.Err(), context.DeadlineExceeded) { - return nil, buserr.New(constant.ErrExecTimeOut) - } - var rows []string - for results.Next() { - var host string - if err := results.Scan(&host); err != nil { - continue - } - rows = append(rows, host) - } - return rows, nil -} - -func loadImage(dbType, version string) (string, error) { - cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - if err != nil { - return "", err - } - images, err := cli.ImageList(context.Background(), image.ListOptions{}) - if err != nil { - return "", err - } - - for _, image := range images { - for _, tag := range image.RepoTags { - if !strings.HasPrefix(tag, dbType+":") { - continue - } - if dbType == "mariadb" && strings.HasPrefix(tag, "mariadb:") { - return tag, nil - } - if strings.HasPrefix(version, "5.6") && strings.HasPrefix(tag, "mysql:5.6") { - return tag, nil - } - if strings.HasPrefix(version, "5.7") && strings.HasPrefix(tag, "mysql:5.7") { - return tag, nil - } - if strings.HasPrefix(version, "8.") && strings.HasPrefix(tag, "mysql:8.") { - return tag, nil - } - } - } - return loadVersion(dbType, version), nil -} - -func loadVersion(dbType string, version string) string { - if dbType == "mariadb" { - return "mariadb:11.3.2 " - } - if strings.HasPrefix(version, "5.6") { - return "mysql:5.6.51" - } - if strings.HasPrefix(version, "5.7") { - return "mysql:5.7.44" - } - return "mysql:8.2.0" -} - -func sslSkip(version, dbType string) string { - if dbType == constant.AppMariaDB || strings.HasPrefix(version, "5.6") || strings.HasPrefix(version, "5.7") { - return "--skip-ssl" - } - return "--ssl-mode=DISABLED" -} diff --git a/backend/utils/mysql/helper/dump.go b/backend/utils/mysql/helper/dump.go deleted file mode 100644 index df0144592..000000000 --- a/backend/utils/mysql/helper/dump.go +++ /dev/null @@ -1,303 +0,0 @@ -package helper - -import ( - "bufio" - "database/sql" - "fmt" - "io" - "os" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - _ "github.com/go-sql-driver/mysql" -) - -func init() {} - -type dumpOption struct { - isData bool - - tables []string - isAllTable bool - isDropTable bool - writer io.Writer -} - -type DumpOption func(*dumpOption) - -func WithDropTable() DumpOption { - return func(option *dumpOption) { - option.isDropTable = true - } -} - -func WithData() DumpOption { - return func(option *dumpOption) { - option.isData = true - } -} - -func WithWriter(writer io.Writer) DumpOption { - return func(option *dumpOption) { - option.writer = writer - } -} - -func Dump(dns string, opts ...DumpOption) error { - start := time.Now() - global.LOG.Infof("dump start at %s\n", start.Format(constant.DateTimeLayout)) - defer func() { - end := time.Now() - global.LOG.Infof("dump end at %s, cost %s\n", end.Format(constant.DateTimeLayout), end.Sub(start)) - }() - - var err error - - var o dumpOption - - for _, opt := range opts { - opt(&o) - } - - if len(o.tables) == 0 { - o.isAllTable = true - } - - if o.writer == nil { - o.writer = os.Stdout - } - - buf := bufio.NewWriter(o.writer) - defer buf.Flush() - - itemFile, lineNumber := "", 0 - - itemFile += "-- ----------------------------\n" - itemFile += "-- MySQL Database Dump\n" - itemFile += "-- Start Time: " + start.Format(constant.DateTimeLayout) + "\n" - itemFile += "-- ----------------------------\n\n\n" - - db, err := sql.Open("mysql", dns) - if err != nil { - global.LOG.Errorf("open mysql db failed, err: %v", err) - return err - } - defer db.Close() - - dbName, err := getDBNameFromDNS(dns) - if err != nil { - global.LOG.Errorf("get db name from dns failed, err: %v", err) - return err - } - _, err = db.Exec(fmt.Sprintf("USE `%s`", dbName)) - if err != nil { - global.LOG.Errorf("exec `use %s` failed, err: %v", dbName, err) - return err - } - - var tables []string - if o.isAllTable { - tmp, err := getAllTables(db) - if err != nil { - global.LOG.Errorf("get all tables failed, err: %v", err) - return err - } - tables = tmp - } else { - tables = o.tables - } - - for _, table := range tables { - if o.isDropTable { - itemFile += fmt.Sprintf("DROP TABLE IF EXISTS `%s`;\n", table) - } - - itemFile += "-- ----------------------------\n" - itemFile += fmt.Sprintf("-- Table structure for %s\n", table) - itemFile += "-- ----------------------------\n" - - createTableSQL, err := getCreateTableSQL(db, table) - if err != nil { - global.LOG.Errorf("get create table sql failed, err: %v", err) - return err - } - itemFile += createTableSQL - itemFile += ";\n\n\n\n" - - if o.isData { - itemFile += "-- ----------------------------\n" - itemFile += fmt.Sprintf("-- Records of %s\n", table) - itemFile += "-- ----------------------------\n" - - lineRows, err := db.Query(fmt.Sprintf("SELECT * FROM `%s`", table)) - if err != nil { - global.LOG.Errorf("exec `select * from %s` failed, err: %v", table, err) - return err - } - defer lineRows.Close() - - var columns []string - columns, err = lineRows.Columns() - if err != nil { - global.LOG.Errorf("get columes failed, err: %v", err) - return err - } - columnTypes, err := lineRows.ColumnTypes() - if err != nil { - global.LOG.Errorf("get colume types failed, err: %v", err) - return err - } - for lineRows.Next() { - row := make([]interface{}, len(columns)) - rowPointers := make([]interface{}, len(columns)) - for i := range columns { - rowPointers[i] = &row[i] - } - if err = lineRows.Scan(rowPointers...); err != nil { - global.LOG.Errorf("scan row data failed, err: %v", err) - return err - } - ssql := loadDataSql(row, columnTypes, table) - if len(ssql) != 0 { - itemFile += ssql - lineNumber++ - } - if lineNumber > 500 { - _, _ = buf.WriteString(itemFile) - itemFile = "" - lineNumber = 0 - } - } - - itemFile += "\n\n" - } - } - - itemFile += "-- ----------------------------\n" - itemFile += "-- Dumped by mysqldump\n" - itemFile += "-- Cost Time: " + time.Since(start).String() + "\n" - itemFile += "-- ----------------------------\n" - - _, _ = buf.WriteString(itemFile) - _ = buf.Flush() - - return nil -} - -func getCreateTableSQL(db *sql.DB, table string) (string, error) { - var createTableSQL string - err := db.QueryRow(fmt.Sprintf("SHOW CREATE TABLE `%s`", table)).Scan(&table, &createTableSQL) - if err != nil { - return "", err - } - createTableSQL = strings.Replace(createTableSQL, "CREATE TABLE", "CREATE TABLE IF NOT EXISTS", 1) - return createTableSQL, nil -} - -func getAllTables(db *sql.DB) ([]string, error) { - var tables []string - rows, err := db.Query("SHOW TABLES") - if err != nil { - return nil, err - } - defer rows.Close() - - for rows.Next() { - var table string - err = rows.Scan(&table) - if err != nil { - return nil, err - } - tables = append(tables, table) - } - return tables, nil -} - -func loadDataSql(row []interface{}, columnTypes []*sql.ColumnType, table string) string { - ssql := "INSERT INTO `" + table + "` VALUES (" - for i, col := range row { - if col == nil { - ssql += "NULL" - } else { - Type := columnTypes[i].DatabaseTypeName() - Type = strings.Replace(Type, "UNSIGNED", "", -1) - Type = strings.Replace(Type, " ", "", -1) - switch Type { - case "TINYINT", "SMALLINT", "MEDIUMINT", "INT", "INTEGER", "BIGINT": - if bs, ok := col.([]byte); ok { - ssql += string(bs) - } else { - ssql += fmt.Sprintf("%d", col) - } - case "FLOAT", "DOUBLE": - if bs, ok := col.([]byte); ok { - ssql += string(bs) - } else { - ssql += fmt.Sprintf("%f", col) - } - case "DECIMAL", "DEC": - ssql += fmt.Sprintf("%s", col) - - case "DATE": - t, ok := col.(time.Time) - if !ok { - global.LOG.Errorf("the DATE type conversion failed, err value: %v", col) - return "" - } - ssql += fmt.Sprintf("'%s'", t.Format("2006-01-02")) - case "DATETIME": - t, ok := col.(time.Time) - if !ok { - global.LOG.Errorf("the DATETIME type conversion failed, err value: %v", col) - return "" - } - ssql += fmt.Sprintf("'%s'", t.Format(constant.DateTimeLayout)) - case "TIMESTAMP": - t, ok := col.(time.Time) - if !ok { - global.LOG.Errorf("the TIMESTAMP type conversion failed, err value: %v", col) - return "" - } - ssql += fmt.Sprintf("'%s'", t.Format(constant.DateTimeLayout)) - case "TIME": - t, ok := col.([]byte) - if !ok { - global.LOG.Errorf("the TIME type conversion failed, err value: %v", col) - return "" - } - ssql += fmt.Sprintf("'%s'", string(t)) - case "YEAR": - t, ok := col.([]byte) - if !ok { - global.LOG.Errorf("the YEAR type conversion failed, err value: %v", col) - return "" - } - ssql += string(t) - case "CHAR", "VARCHAR", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT": - ssql += fmt.Sprintf("'%s'", strings.Replace(fmt.Sprintf("%s", col), "'", "''", -1)) - case "BIT", "BINARY", "VARBINARY", "TINYBLOB", "BLOB", "MEDIUMBLOB", "LONGBLOB": - ssql += fmt.Sprintf("0x%X", col) - case "ENUM", "SET": - ssql += fmt.Sprintf("'%s'", col) - case "BOOL", "BOOLEAN": - if col.(bool) { - ssql += "true" - } else { - ssql += "false" - } - case "JSON": - ssql += fmt.Sprintf("'%s'", col) - default: - global.LOG.Errorf("unsupported colume type: %s", Type) - return "" - } - } - if i < len(row)-1 { - ssql += "," - } - } - ssql += ");\n" - return ssql -} diff --git a/backend/utils/mysql/helper/source.go b/backend/utils/mysql/helper/source.go deleted file mode 100644 index b15a5cd97..000000000 --- a/backend/utils/mysql/helper/source.go +++ /dev/null @@ -1,244 +0,0 @@ -package helper - -import ( - "bufio" - "database/sql" - "errors" - "fmt" - "io" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" -) - -type sourceOption struct { - dryRun bool - mergeInsert int - debug bool -} -type SourceOption func(*sourceOption) - -func WithMergeInsert(size int) SourceOption { - return func(o *sourceOption) { - o.mergeInsert = size - } -} - -type dbWrapper struct { - DB *sql.DB - debug bool - dryRun bool -} - -func newDBWrapper(db *sql.DB, dryRun, debug bool) *dbWrapper { - - return &dbWrapper{ - DB: db, - dryRun: dryRun, - debug: debug, - } -} - -func (db *dbWrapper) Exec(query string, args ...interface{}) (sql.Result, error) { - if db.debug { - global.LOG.Debugf("query %s", query) - } - - if db.dryRun { - return nil, nil - } - return db.DB.Exec(query, args...) -} - -func Source(dns string, reader io.Reader, opts ...SourceOption) error { - start := time.Now() - global.LOG.Infof("source start at %s", start.Format(constant.DateTimeLayout)) - defer func() { - end := time.Now() - global.LOG.Infof("source end at %s, cost %s", end.Format(constant.DateTimeLayout), end.Sub(start)) - }() - - var err error - var db *sql.DB - var o sourceOption - for _, opt := range opts { - opt(&o) - } - - dbName, err := getDBNameFromDNS(dns) - if err != nil { - global.LOG.Errorf("get db name from dns failed, err: %v", err) - return err - } - - db, err = sql.Open("mysql", dns) - if err != nil { - global.LOG.Errorf("open mysql db failed, err: %v", err) - return err - } - defer db.Close() - - dbWrapper := newDBWrapper(db, o.dryRun, o.debug) - - _, err = dbWrapper.Exec(fmt.Sprintf("USE `%s`;", dbName)) - if err != nil { - global.LOG.Errorf("exec `use %s` failed, err: %v", dbName, err) - return err - } - - db.SetConnMaxLifetime(3600) - - r := bufio.NewReader(reader) - _, err = dbWrapper.Exec("SET autocommit=0;") - if err != nil { - global.LOG.Errorf("exec `set autocommit=0` failed, err: %v", err) - return err - } - - for { - line, err := readLine(r) - if err != nil { - if err == io.EOF { - break - } - global.LOG.Errorf("read sql failed, err: %v", err) - return err - } - - ssql, err := trim(line) - if err != nil { - global.LOG.Errorf("trim sql failed, err: %v", err) - return err - } - - afterInsertSql := "" - if o.mergeInsert > 1 && strings.HasPrefix(ssql, "INSERT INTO") { - var insertSQLs []string - insertSQLs = append(insertSQLs, ssql) - for i := 0; i < o.mergeInsert-1; i++ { - line, err := readLine(r) - if err != nil { - if err == io.EOF { - break - } - return err - } - ssql2, err := trim(line) - if err != nil { - global.LOG.Errorf("trim merge insert sql failed, err: %v", err) - return err - } - if strings.HasPrefix(ssql2, "INSERT INTO") { - insertSQLs = append(insertSQLs, ssql2) - continue - } - afterInsertSql = ssql2 - break - } - ssql, err = mergeInsert(insertSQLs) - if err != nil { - global.LOG.Errorf("do merge insert failed, err: %v", err) - return err - } - } - - _, err = dbWrapper.Exec(ssql) - if err != nil { - global.LOG.Errorf("exec sql failed, err: %v", err) - return err - } - if len(afterInsertSql) != 0 { - _, err = dbWrapper.Exec(afterInsertSql) - if err != nil { - global.LOG.Errorf("exec sql failed, err: %v", err) - return err - } - } - } - - _, err = dbWrapper.Exec("COMMIT;") - if err != nil { - global.LOG.Errorf("exec `commit` failed, err: %v", err) - return err - } - - _, err = dbWrapper.Exec("SET autocommit=1;") - if err != nil { - global.LOG.Errorf("exec `autocommit=1` failed, err: %v", err) - return err - } - - return nil -} - -func mergeInsert(insertSQLs []string) (string, error) { - if len(insertSQLs) == 0 { - return "", errors.New("no input provided") - } - builder := strings.Builder{} - sql1 := insertSQLs[0] - sql1 = strings.TrimSuffix(sql1, ";") - builder.WriteString(sql1) - for i, insertSQL := range insertSQLs[1:] { - if i < len(insertSQLs)-1 { - builder.WriteString(",") - } - - valuesIdx := strings.Index(insertSQL, "VALUES") - if valuesIdx == -1 { - return "", errors.New("invalid SQL: missing VALUES keyword") - } - sqln := insertSQL[valuesIdx:] - sqln = strings.TrimPrefix(sqln, "VALUES") - sqln = strings.TrimSuffix(sqln, ";") - builder.WriteString(sqln) - - } - builder.WriteString(";") - - return builder.String(), nil -} - -func trim(s string) (string, error) { - s = strings.TrimLeft(s, "\n") - s = strings.TrimSpace(s) - return s, nil -} - -func getDBNameFromDNS(dns string) (string, error) { - ss1 := strings.Split(dns, "/") - if len(ss1) == 2 { - ss2 := strings.Split(ss1[1], "?") - if len(ss2) == 2 { - return ss2[0], nil - } - } - - return "", fmt.Errorf("dns error: %s", dns) -} - -func readLine(r *bufio.Reader) (string, error) { - lineItem, err := r.ReadString('\n') - if err != nil { - if err == io.EOF { - return lineItem, err - } - global.LOG.Errorf("read merge insert sql failed, err: %v", err) - return "", err - } - if strings.HasSuffix(lineItem, ";\n") { - return lineItem, nil - } - lineAppend, err := readLine(r) - if err != nil { - if err == io.EOF { - return lineItem, err - } - global.LOG.Errorf("read merge insert sql failed, err: %v", err) - return "", err - } - - return lineItem + lineAppend, nil -} diff --git a/backend/utils/nginx/components/block.go b/backend/utils/nginx/components/block.go deleted file mode 100644 index 0e07e7793..000000000 --- a/backend/utils/nginx/components/block.go +++ /dev/null @@ -1,88 +0,0 @@ -package components - -type Block struct { - Line int - Comment string - Directives []IDirective - IsLuaBlock bool - LiteralCode string -} - -func (b *Block) GetDirectives() []IDirective { - return b.Directives -} - -func (b *Block) GetComment() string { - return b.Comment -} - -func (b *Block) GetLine() int { - return b.Line -} - -func (b *Block) GetCodeBlock() string { - return b.LiteralCode -} - -func (b *Block) FindDirectives(directiveName string) []IDirective { - directives := make([]IDirective, 0) - for _, directive := range b.GetDirectives() { - if directive.GetName() == directiveName { - directives = append(directives, directive) - } - if directive.GetBlock() != nil { - directives = append(directives, directive.GetBlock().FindDirectives(directiveName)...) - } - } - - return directives -} - -func (b *Block) UpdateDirective(key string, params []string) { - if key == "" || len(params) == 0 { - return - } - directives := b.GetDirectives() - index := -1 - for i, dir := range directives { - if dir.GetName() == key { - if IsRepeatKey(key) { - oldParams := dir.GetParameters() - if !(len(oldParams) > 0 && oldParams[0] == params[0]) { - continue - } - } - index = i - break - } - } - newDirective := &Directive{ - Name: key, - Parameters: params, - } - if index > -1 { - directives[index] = newDirective - } else { - directives = append(directives, newDirective) - } - b.Directives = directives -} - -func (b *Block) RemoveDirective(key string, params []string) { - directives := b.GetDirectives() - var newDirectives []IDirective - for _, dir := range directives { - if dir.GetName() == key { - if IsRepeatKey(key) && len(params) > 0 { - oldParams := dir.GetParameters() - if oldParams[0] == params[0] { - continue - } - } else { - continue - } - } - newDirectives = append(newDirectives, dir) - } - b.Directives = newDirectives -} diff --git a/backend/utils/nginx/components/comment.go b/backend/utils/nginx/components/comment.go deleted file mode 100644 index 31459be09..000000000 --- a/backend/utils/nginx/components/comment.go +++ /dev/null @@ -1,26 +0,0 @@ -package components - -type Comment struct { - Detail string - Line int -} - -func (c *Comment) GetName() string { - return "" -} - -func (c *Comment) GetParameters() []string { - return []string{} -} - -func (c *Comment) GetBlock() IBlock { - return nil -} - -func (c *Comment) GetComment() string { - return c.Detail -} - -func (c *Comment) GetLine() int { - return c.Line -} diff --git a/backend/utils/nginx/components/config.go b/backend/utils/nginx/components/config.go deleted file mode 100644 index 7f9b5bce9..000000000 --- a/backend/utils/nginx/components/config.go +++ /dev/null @@ -1,45 +0,0 @@ -package components - -type Config struct { - *Block - FilePath string -} - -func (c *Config) FindServers() []*Server { - var servers []*Server - directives := c.Block.FindDirectives("server") - for _, directive := range directives { - servers = append(servers, directive.(*Server)) - } - return servers -} - -func (c *Config) FindHttp() *Http { - var http *Http - directives := c.Block.FindDirectives("http") - if len(directives) > 0 { - http = directives[0].(*Http) - } - - return http -} - -var repeatKeys = map[string]struct { -}{ - "limit_conn": {}, - "limit_conn_zone": {}, - "set": {}, - "if": {}, - "proxy_set_header": {}, - "location": {}, - "include": {}, - "sub_filter": {}, - "add_header": {}, -} - -func IsRepeatKey(key string) bool { - if _, ok := repeatKeys[key]; ok { - return true - } - return false -} diff --git a/backend/utils/nginx/components/directive.go b/backend/utils/nginx/components/directive.go deleted file mode 100644 index dfdef75ef..000000000 --- a/backend/utils/nginx/components/directive.go +++ /dev/null @@ -1,29 +0,0 @@ -package components - -type Directive struct { - Line int - Block IBlock - Name string - Comment string - Parameters []string -} - -func (d *Directive) GetComment() string { - return d.Comment -} - -func (d *Directive) GetName() string { - return d.Name -} - -func (d *Directive) GetParameters() []string { - return d.Parameters -} - -func (d *Directive) GetBlock() IBlock { - return d.Block -} - -func (d *Directive) GetLine() int { - return d.Line -} diff --git a/backend/utils/nginx/components/http.go b/backend/utils/nginx/components/http.go deleted file mode 100644 index ff9d3028c..000000000 --- a/backend/utils/nginx/components/http.go +++ /dev/null @@ -1,129 +0,0 @@ -package components - -import ( - "errors" -) - -type Http struct { - Comment string - Servers []*Server - Directives []IDirective - Line int -} - -func (h *Http) GetCodeBlock() string { - return "" -} - -func (h *Http) GetComment() string { - return h.Comment -} - -func NewHttp(directive IDirective) (*Http, error) { - if block := directive.GetBlock(); block != nil { - http := &Http{ - Line: directive.GetBlock().GetLine(), - Servers: []*Server{}, - Directives: []IDirective{}, - Comment: block.GetComment(), - } - for _, directive := range block.GetDirectives() { - if server, ok := directive.(*Server); ok { - http.Servers = append(http.Servers, server) - continue - } - http.Directives = append(http.Directives, directive) - } - - return http, nil - } - return nil, errors.New("http directive must have a block") -} - -func (h *Http) GetName() string { - return "http" -} - -func (h *Http) GetParameters() []string { - return []string{} -} - -func (h *Http) GetDirectives() []IDirective { - directives := make([]IDirective, 0) - directives = append(directives, h.Directives...) - for _, directive := range h.Servers { - directives = append(directives, directive) - } - return directives -} - -func (h *Http) FindDirectives(directiveName string) []IDirective { - directives := make([]IDirective, 0) - for _, directive := range h.GetDirectives() { - if directive.GetName() == directiveName { - directives = append(directives, directive) - } - if directive.GetBlock() != nil { - directives = append(directives, directive.GetBlock().FindDirectives(directiveName)...) - } - } - - return directives -} - -func (h *Http) UpdateDirective(key string, params []string) { - if key == "" || len(params) == 0 { - return - } - directives := h.GetDirectives() - index := -1 - for i, dir := range directives { - if dir.GetName() == key { - if IsRepeatKey(key) { - oldParams := dir.GetParameters() - if !(len(oldParams) > 0 && oldParams[0] == params[0]) { - continue - } - } - index = i - break - } - } - newDirective := &Directive{ - Name: key, - Parameters: params, - } - if index > -1 { - directives[index] = newDirective - } else { - directives = append(directives, newDirective) - } - h.Directives = directives -} - -func (h *Http) RemoveDirective(key string, params []string) { - directives := h.GetDirectives() - var newDirectives []IDirective - for _, dir := range directives { - if dir.GetName() == key { - if IsRepeatKey(key) && len(params) > 0 { - oldParams := dir.GetParameters() - if oldParams[0] == params[0] { - continue - } - } else { - continue - } - } - newDirectives = append(newDirectives, dir) - } - h.Directives = newDirectives -} - -func (h *Http) GetBlock() IBlock { - return h -} - -func (h *Http) GetLine() int { - return h.Line -} diff --git a/backend/utils/nginx/components/location.go b/backend/utils/nginx/components/location.go deleted file mode 100644 index c407c0ede..000000000 --- a/backend/utils/nginx/components/location.go +++ /dev/null @@ -1,244 +0,0 @@ -package components - -import ( - "fmt" - "regexp" - "strconv" - "strings" -) - -type Location struct { - Modifier string - Match string - Cache bool - ProxyPass string - Host string - CacheTime int - CacheUint string - Comment string - Directives []IDirective - Line int - Parameters []string - Replaces map[string]string -} - -func (l *Location) GetCodeBlock() string { - return "" -} - -func NewLocation(directive IDirective) *Location { - location := &Location{ - Modifier: "", - Match: "", - } - directives := make([]IDirective, 0) - if len(directive.GetParameters()) == 0 { - panic("no enough parameter for location") - } - for _, dir := range directive.GetBlock().GetDirectives() { - directives = append(directives, dir) - params := dir.GetParameters() - switch dir.GetName() { - case "proxy_pass": - location.ProxyPass = params[0] - case "proxy_set_header": - if params[0] == "Host" { - location.Host = params[1] - } - case "proxy_cache": - location.Cache = true - case "if": - if params[0] == "(" && params[1] == "$uri" && params[2] == "~*" { - dirs := dir.GetBlock().GetDirectives() - for _, di := range dirs { - if di.GetName() == "expires" { - re := regexp.MustCompile(`^(\d+)(\w+)$`) - matches := re.FindStringSubmatch(di.GetParameters()[0]) - if matches == nil { - continue - } - cacheTime, err := strconv.Atoi(matches[1]) - if err != nil { - continue - } - unit := matches[2] - location.CacheUint = unit - location.CacheTime = cacheTime - } - } - } - case "sub_filter": - if location.Replaces == nil { - location.Replaces = make(map[string]string, 0) - } - location.Replaces[strings.Trim(params[0], "\"")] = strings.Trim(params[1], "\"") - } - } - - params := directive.GetParameters() - if len(params) == 1 { - location.Match = params[0] - } else if len(params) == 2 { - location.Match = params[1] - location.Modifier = params[0] - } - location.Parameters = directive.GetParameters() - location.Line = directive.GetLine() - location.Comment = directive.GetComment() - location.Directives = directives - return location -} - -func (l *Location) GetName() string { - return "location" -} - -func (l *Location) GetParameters() []string { - return l.Parameters -} - -func (l *Location) GetBlock() IBlock { - return l -} - -func (l *Location) GetComment() string { - return l.Comment -} - -func (l *Location) GetLine() int { - return l.Line -} - -func (l *Location) GetDirectives() []IDirective { - return l.Directives -} - -func (l *Location) FindDirectives(directiveName string) []IDirective { - directives := make([]IDirective, 0) - for _, directive := range l.Directives { - if directive.GetName() == directiveName { - directives = append(directives, directive) - } - if directive.GetBlock() != nil { - directives = append(directives, directive.GetBlock().FindDirectives(directiveName)...) - } - } - return directives -} - -func (l *Location) UpdateDirective(key string, params []string) { - if key == "" || len(params) == 0 { - return - } - directives := l.Directives - index := -1 - for i, dir := range directives { - if dir.GetName() == key { - if IsRepeatKey(key) { - oldParams := dir.GetParameters() - if !(len(oldParams) > 0 && oldParams[0] == params[0]) { - continue - } - } - index = i - break - } - } - newDirective := &Directive{ - Name: key, - Parameters: params, - } - if index > -1 { - directives[index] = newDirective - } else { - directives = append(directives, newDirective) - } - l.Directives = directives -} - -func (l *Location) RemoveDirective(key string, params []string) { - directives := l.Directives - var newDirectives []IDirective - for _, dir := range directives { - if dir.GetName() == key { - if len(params) > 0 { - oldParams := dir.GetParameters() - if oldParams[0] == params[0] { - continue - } - } else { - continue - } - } - newDirectives = append(newDirectives, dir) - } - l.Directives = newDirectives -} - -func (l *Location) ChangePath(Modifier string, Match string) { - if Match != "" && Modifier != "" { - l.Parameters = []string{Modifier, Match} - } - if Match != "" && Modifier == "" { - l.Parameters = []string{Match} - } - l.Modifier = Modifier - l.Match = Match -} - -func (l *Location) AddCache(cacheTime int, cacheUint string) { - l.RemoveDirective("add_header", []string{"Cache-Control", "no-cache"}) - l.RemoveDirective("if", []string{"(", "$uri", "~*", `"\.(gif|png|jpg|css|js|woff|woff2)$"`, ")"}) - directives := l.GetDirectives() - newDir := &Directive{ - Name: "if", - Parameters: []string{"(", "$uri", "~*", `"\.(gif|png|jpg|css|js|woff|woff2)$"`, ")"}, - Block: &Block{}, - } - block := &Block{} - block.Directives = append(block.Directives, &Directive{ - Name: "expires", - Parameters: []string{strconv.Itoa(cacheTime) + cacheUint}, - }) - newDir.Block = block - directives = append(directives, newDir) - l.Directives = directives - l.UpdateDirective("proxy_ignore_headers", []string{"Set-Cookie", "Cache-Control", "expires"}) - l.UpdateDirective("proxy_cache", []string{"proxy_cache_panel"}) - l.UpdateDirective("proxy_cache_key", []string{"$host$uri$is_args$args"}) - l.UpdateDirective("proxy_cache_valid", []string{"200", "304", "301", "302", "10m"}) - l.Cache = true - l.CacheTime = cacheTime - l.CacheUint = cacheUint -} - -func (l *Location) RemoveCache() { - l.RemoveDirective("if", []string{"(", "$uri", "~*", `"\.(gif|png|jpg|css|js|woff|woff2)$"`, ")"}) - l.RemoveDirective("proxy_ignore_headers", []string{"Set-Cookie"}) - l.RemoveDirective("proxy_cache", []string{"proxy_cache_panel"}) - l.RemoveDirective("proxy_cache_key", []string{"$host$uri$is_args$args"}) - l.RemoveDirective("proxy_cache_valid", []string{"200"}) - - l.UpdateDirective("add_header", []string{"Cache-Control", "no-cache"}) - - l.CacheTime = 0 - l.CacheUint = "" - l.Cache = false -} - -func (l *Location) AddSubFilter(subFilters map[string]string) { - l.RemoveDirective("sub_filter", []string{}) - l.Replaces = subFilters - for k, v := range subFilters { - l.UpdateDirective("sub_filter", []string{fmt.Sprintf(`"%s"`, k), fmt.Sprintf(`"%s"`, v)}) - } - l.UpdateDirective("proxy_set_header", []string{"Accept-Encoding", `""`}) - l.UpdateDirective("sub_filter_once", []string{"off"}) -} - -func (l *Location) RemoveSubFilter() { - l.RemoveDirective("sub_filter", []string{}) - l.RemoveDirective("proxy_set_header", []string{"Accept-Encoding", `""`}) - l.RemoveDirective("sub_filter_once", []string{"off"}) - l.Replaces = nil -} diff --git a/backend/utils/nginx/components/lua_block.go b/backend/utils/nginx/components/lua_block.go deleted file mode 100644 index d9b4b3676..000000000 --- a/backend/utils/nginx/components/lua_block.go +++ /dev/null @@ -1,120 +0,0 @@ -package components - -import ( - "fmt" -) - -type LuaBlock struct { - Directives []IDirective - Name string - Comment string - LuaCode string - Line int -} - -func NewLuaBlock(directive IDirective) (*LuaBlock, error) { - if block := directive.GetBlock(); block != nil { - lb := &LuaBlock{ - Directives: []IDirective{}, - Name: directive.GetName(), - LuaCode: block.GetCodeBlock(), - } - - lb.Directives = append(lb.Directives, block.GetDirectives()...) - return lb, nil - } - return nil, fmt.Errorf("%s must have a block", directive.GetName()) -} - -func (lb *LuaBlock) GetName() string { - return lb.Name -} - -func (lb *LuaBlock) GetParameters() []string { - return []string{} -} - -func (lb *LuaBlock) GetDirectives() []IDirective { - directives := make([]IDirective, 0) - directives = append(directives, lb.Directives...) - return directives -} - -func (lb *LuaBlock) FindDirectives(directiveName string) []IDirective { - directives := make([]IDirective, 0) - for _, directive := range lb.GetDirectives() { - if directive.GetName() == directiveName { - directives = append(directives, directive) - } - if directive.GetBlock() != nil { - directives = append(directives, directive.GetBlock().FindDirectives(directiveName)...) - } - } - - return directives -} - -func (lb *LuaBlock) GetCodeBlock() string { - return lb.LuaCode -} - -func (lb *LuaBlock) GetBlock() IBlock { - return lb -} - -func (lb *LuaBlock) GetComment() string { - return lb.Comment -} - -func (lb *LuaBlock) RemoveDirective(key string, params []string) { - directives := lb.Directives - var newDirectives []IDirective - for _, dir := range directives { - if dir.GetName() == key { - if len(params) > 0 { - oldParams := dir.GetParameters() - if oldParams[0] == params[0] { - continue - } - } else { - continue - } - } - newDirectives = append(newDirectives, dir) - } - lb.Directives = newDirectives -} - -func (lb *LuaBlock) UpdateDirective(key string, params []string) { - if key == "" || len(params) == 0 { - return - } - directives := lb.Directives - index := -1 - for i, dir := range directives { - if dir.GetName() == key { - if IsRepeatKey(key) { - oldParams := dir.GetParameters() - if !(len(oldParams) > 0 && oldParams[0] == params[0]) { - continue - } - } - index = i - break - } - } - newDirective := &Directive{ - Name: key, - Parameters: params, - } - if index > -1 { - directives[index] = newDirective - } else { - directives = append(directives, newDirective) - } - lb.Directives = directives -} - -func (lb *LuaBlock) GetLine() int { - return lb.Line -} diff --git a/backend/utils/nginx/components/server.go b/backend/utils/nginx/components/server.go deleted file mode 100644 index c7138936f..000000000 --- a/backend/utils/nginx/components/server.go +++ /dev/null @@ -1,371 +0,0 @@ -package components - -import ( - "errors" -) - -type Server struct { - Comment string - Listens []*ServerListen - Directives []IDirective - Line int -} - -func (s *Server) GetCodeBlock() string { - return "" -} - -func NewServer(directive IDirective) (*Server, error) { - server := &Server{} - if block := directive.GetBlock(); block != nil { - server.Line = directive.GetBlock().GetLine() - server.Comment = block.GetComment() - directives := block.GetDirectives() - for _, dir := range directives { - switch dir.GetName() { - case "listen": - server.Listens = append(server.Listens, NewServerListen(dir.GetParameters(), dir.GetLine())) - default: - server.Directives = append(server.Directives, dir) - } - } - return server, nil - } - return nil, errors.New("server directive must have a block") -} - -func (s *Server) GetName() string { - return "server" -} - -func (s *Server) GetParameters() []string { - return []string{} -} - -func (s *Server) GetBlock() IBlock { - return s -} - -func (s *Server) GetComment() string { - return s.Comment -} - -func (s *Server) GetDirectives() []IDirective { - directives := make([]IDirective, 0) - for _, ls := range s.Listens { - directives = append(directives, ls) - } - directives = append(directives, s.Directives...) - return directives -} - -func (s *Server) FindDirectives(directiveName string) []IDirective { - directives := make([]IDirective, 0) - for _, directive := range s.Directives { - if directive.GetName() == directiveName { - directives = append(directives, directive) - } - if directive.GetBlock() != nil { - directives = append(directives, directive.GetBlock().FindDirectives(directiveName)...) - } - } - if directiveName == "listen" { - for _, listen := range s.Listens { - params := []string{listen.Bind} - params = append(params, listen.Parameters...) - if listen.DefaultServer != "" { - params = append(params, DefaultServer) - } - directives = append(directives, &Directive{ - Name: "listen", - Parameters: params, - }) - } - } - return directives -} - -func (s *Server) UpdateDirective(key string, params []string) { - if key == "" || len(params) == 0 { - return - } - if key == "listen" { - defaultServer := false - paramLen := len(params) - if paramLen > 0 && params[paramLen-1] == "default_server" { - params = params[:paramLen-1] - defaultServer = true - } - s.UpdateListen(params[0], defaultServer, params[1:]...) - return - } - - directives := s.Directives - index := -1 - for i, dir := range directives { - if dir.GetName() == key { - if IsRepeatKey(key) { - oldParams := dir.GetParameters() - if !(len(oldParams) > 0 && oldParams[0] == params[0]) { - continue - } - } - index = i - break - } - } - newDirective := &Directive{ - Name: key, - Parameters: params, - } - if index > -1 { - directives[index] = newDirective - } else { - directives = append(directives, newDirective) - } - s.Directives = directives -} - -func (s *Server) RemoveDirective(key string, params []string) { - directives := s.Directives - var newDirectives []IDirective - for _, dir := range directives { - if dir.GetName() == key { - if len(params) == 0 { - continue - } - oldParams := dir.GetParameters() - if key == "location" { - if len(params) == len(oldParams) { - exist := true - for i := range params { - if params[i] != oldParams[i] { - exist = false - break - } - } - if exist { - continue - } - } - } else { - if oldParams[0] == params[0] { - continue - } - } - } - newDirectives = append(newDirectives, dir) - } - s.Directives = newDirectives -} - -func (s *Server) GetLine() int { - return s.Line -} - -func (s *Server) AddListen(bind string, defaultServer bool, params ...string) { - listen := &ServerListen{ - Bind: bind, - Parameters: params, - } - if defaultServer { - listen.DefaultServer = DefaultServer - } - s.Listens = append(s.Listens, listen) -} - -func (s *Server) UpdateListen(bind string, defaultServer bool, params ...string) { - listen := &ServerListen{ - Bind: bind, - Parameters: params, - } - if defaultServer { - listen.DefaultServer = DefaultServer - } - var newListens []*ServerListen - exist := false - for _, li := range s.Listens { - if li.Bind == bind { - exist = true - newListens = append(newListens, listen) - } else { - newListens = append(newListens, li) - } - } - if !exist { - newListens = append(newListens, listen) - } - - s.Listens = newListens -} - -func (s *Server) DeleteListen(bind string) { - var newListens []*ServerListen - for _, li := range s.Listens { - if li.Bind != bind { - newListens = append(newListens, li) - } - } - s.Listens = newListens -} - -func (s *Server) DeleteServerName(name string) { - var names []string - dirs := s.FindDirectives("server_name") - params := dirs[0].GetParameters() - for _, param := range params { - if param != name { - names = append(names, param) - } - } - s.UpdateServerName(names) -} - -func (s *Server) AddServerName(name string) { - dirs := s.FindDirectives("server_name") - params := dirs[0].GetParameters() - params = append(params, name) - s.UpdateServerName(params) -} - -func (s *Server) UpdateServerName(names []string) { - s.UpdateDirective("server_name", names) -} - -func (s *Server) UpdateRoot(path string) { - s.UpdateDirective("root", []string{path}) -} - -func (s *Server) UpdateRootLocation() { - newDir := Directive{ - Name: "location", - Parameters: []string{"/"}, - Block: &Block{}, - } - block := &Block{} - block.Directives = append(block.Directives, &Directive{ - Name: "root", - Parameters: []string{"index.html"}, - }) - newDir.Block = block -} - -func (s *Server) UpdateRootProxy(proxy []string) { - newDir := Directive{ - Name: "location", - Parameters: []string{"/"}, - Block: &Block{}, - } - block := &Block{} - block.Directives = append(block.Directives, &Directive{ - Name: "proxy_pass", - Parameters: proxy, - }) - newDir.Block = block - s.UpdateDirectiveBySecondKey("location", "/", newDir) -} - -func (s *Server) UpdatePHPProxy(proxy []string, localPath string) { - newDir := Directive{ - Name: "location", - Parameters: []string{"~ [^/]\\.php(/|$)"}, - Block: &Block{}, - } - block := &Block{} - block.Directives = append(block.Directives, &Directive{ - Name: "fastcgi_pass", - Parameters: proxy, - }) - block.Directives = append(block.Directives, &Directive{ - Name: "include", - Parameters: []string{"fastcgi-php.conf"}, - }) - block.Directives = append(block.Directives, &Directive{ - Name: "include", - Parameters: []string{"fastcgi_params"}, - }) - if localPath == "" { - block.Directives = append(block.Directives, &Directive{ - Name: "set", - Parameters: []string{"$real_script_name", "$fastcgi_script_name"}, - }) - ifDir := &Directive{ - Name: "if", - Parameters: []string{"($fastcgi_script_name ~ \"^(.+?\\.php)(/.+)$\")"}, - } - ifDir.Block = &Block{ - Directives: []IDirective{ - &Directive{ - Name: "set", - Parameters: []string{"$real_script_name", "$1"}, - }, - &Directive{ - Name: "set", - Parameters: []string{"$path_info", "$2"}, - }, - }, - } - block.Directives = append(block.Directives, ifDir) - block.Directives = append(block.Directives, &Directive{ - Name: "fastcgi_param", - Parameters: []string{"SCRIPT_FILENAME", "$document_root$real_script_name"}, - }) - block.Directives = append(block.Directives, &Directive{ - Name: "fastcgi_param", - Parameters: []string{"SCRIPT_NAME", "$real_script_name"}, - }) - block.Directives = append(block.Directives, &Directive{ - Name: "fastcgi_param", - Parameters: []string{"PATH_INFO", "$path_info"}, - }) - } else { - block.Directives = append(block.Directives, &Directive{ - Name: "fastcgi_param", - Parameters: []string{"SCRIPT_FILENAME", localPath}, - }) - } - newDir.Block = block - s.UpdateDirectiveBySecondKey("location", "~ [^/]\\.php(/|$)", newDir) -} - -func (s *Server) UpdateDirectiveBySecondKey(name string, key string, directive Directive) { - directives := s.Directives - index := -1 - for i, dir := range directives { - if dir.GetName() == name && dir.GetParameters()[0] == key { - index = i - break - } - } - if index > -1 { - directives[index] = &directive - } else { - directives = append(directives, &directive) - } - s.Directives = directives -} - -func (s *Server) RemoveListenByBind(bind string) { - var listens []*ServerListen - for _, listen := range s.Listens { - if listen.Bind != bind { - listens = append(listens, listen) - } - } - s.Listens = listens -} - -func (s *Server) AddHTTP2HTTPS() { - newDir := Directive{ - Name: "if", - Parameters: []string{"($scheme = http)"}, - Block: &Block{}, - } - block := &Block{} - block.Directives = append(block.Directives, &Directive{ - Name: "return", - Parameters: []string{"301", "https://$host$request_uri"}, - }) - newDir.Block = block - s.UpdateDirectiveBySecondKey("if", "($scheme", newDir) -} diff --git a/backend/utils/nginx/components/server_listen.go b/backend/utils/nginx/components/server_listen.go deleted file mode 100644 index 8a811be4c..000000000 --- a/backend/utils/nginx/components/server_listen.go +++ /dev/null @@ -1,74 +0,0 @@ -package components - -import ( - "github.com/1Panel-dev/1Panel/backend/utils/common" - "strings" -) - -const DefaultServer = "default_server" - -type ServerListen struct { - Bind string - DefaultServer string - Parameters []string - Comment string - Line int -} - -func NewServerListen(params []string, line int) *ServerListen { - server := &ServerListen{ - Parameters: []string{}, - Line: line, - } - for _, param := range params { - if isBind(param) { - server.Bind = param - } else if param == DefaultServer { - server.DefaultServer = DefaultServer - } else { - server.Parameters = append(server.Parameters, param) - } - } - return server -} - -func isBind(param string) bool { - if common.IsNum(param) { - return true - } - if strings.Contains(param, "*") || strings.Contains(param, ":") || strings.Contains(param, ".") { - return true - } - return false -} - -func (sl *ServerListen) GetName() string { - return "listen" -} - -func (sl *ServerListen) GetBlock() IBlock { - return nil -} - -func (sl *ServerListen) GetParameters() []string { - params := []string{sl.Bind} - params = append(params, sl.Parameters...) - params = append(params, sl.DefaultServer) - return params -} - -func (sl *ServerListen) GetComment() string { - return sl.Comment -} - -func (sl *ServerListen) AddDefaultServer() { - sl.DefaultServer = DefaultServer -} - -func (sl *ServerListen) RemoveDefaultServe() { - sl.DefaultServer = "" -} - -func (sl *ServerListen) GetLine() int { - return sl.Line -} diff --git a/backend/utils/nginx/components/statement.go b/backend/utils/nginx/components/statement.go deleted file mode 100644 index 432a4c165..000000000 --- a/backend/utils/nginx/components/statement.go +++ /dev/null @@ -1,19 +0,0 @@ -package components - -type IBlock interface { - GetDirectives() []IDirective - FindDirectives(directiveName string) []IDirective - RemoveDirective(name string, params []string) - UpdateDirective(name string, params []string) - GetComment() string - GetLine() int - GetCodeBlock() string -} - -type IDirective interface { - GetName() string - GetParameters() []string - GetBlock() IBlock - GetComment() string - GetLine() int -} diff --git a/backend/utils/nginx/components/upstream.go b/backend/utils/nginx/components/upstream.go deleted file mode 100644 index 68e26d42d..000000000 --- a/backend/utils/nginx/components/upstream.go +++ /dev/null @@ -1,135 +0,0 @@ -package components - -import ( - "errors" -) - -type Upstream struct { - UpstreamName string - UpstreamServers []*UpstreamServer - Directives []IDirective - Comment string - Line int -} - -func (us *Upstream) GetCodeBlock() string { - return "" -} - -func (us *Upstream) GetName() string { - return "upstream" -} - -func (us *Upstream) GetParameters() []string { - return []string{us.UpstreamName} -} - -func (us *Upstream) GetBlock() IBlock { - return us -} - -func (us *Upstream) GetComment() string { - return us.Comment -} - -func (us *Upstream) GetDirectives() []IDirective { - directives := make([]IDirective, 0) - directives = append(directives, us.Directives...) - for _, uss := range us.UpstreamServers { - directives = append(directives, uss) - } - return directives -} - -func NewUpstream(directive IDirective) (*Upstream, error) { - parameters := directive.GetParameters() - us := &Upstream{ - UpstreamName: parameters[0], - Line: directive.GetLine(), - } - - if block := directive.GetBlock(); block != nil { - us.Comment = block.GetComment() - for _, d := range block.GetDirectives() { - if d.GetName() == "server" { - us.UpstreamServers = append(us.UpstreamServers, NewUpstreamServer(d)) - } else { - us.Directives = append(us.Directives, d) - } - } - return us, nil - } - - return nil, errors.New("missing upstream block") -} - -func (us *Upstream) AddServer(server *UpstreamServer) { - us.UpstreamServers = append(us.UpstreamServers, server) -} - -func (us *Upstream) FindDirectives(directiveName string) []IDirective { - directives := make([]IDirective, 0) - for _, directive := range us.Directives { - if directive.GetName() == directiveName { - directives = append(directives, directive) - } - if directive.GetBlock() != nil { - directives = append(directives, directive.GetBlock().FindDirectives(directiveName)...) - } - } - - return directives -} - -func (us *Upstream) UpdateDirective(key string, params []string) { - if key == "" || len(params) == 0 { - return - } - directives := us.GetDirectives() - index := -1 - for i, dir := range directives { - if dir.GetName() == key { - if IsRepeatKey(key) { - oldParams := dir.GetParameters() - if !(len(oldParams) > 0 && oldParams[0] == params[0]) { - continue - } - } - index = i - break - } - } - newDirective := &Directive{ - Name: key, - Parameters: params, - } - if index > -1 { - directives[index] = newDirective - } else { - directives = append(directives, newDirective) - } - us.Directives = directives -} - -func (us *Upstream) RemoveDirective(key string, params []string) { - directives := us.GetDirectives() - var newDirectives []IDirective - for _, dir := range directives { - if dir.GetName() == key { - if IsRepeatKey(key) && len(params) > 0 { - oldParams := dir.GetParameters() - if oldParams[0] == params[0] { - continue - } - } else { - continue - } - } - newDirectives = append(newDirectives, dir) - } - us.Directives = newDirectives -} - -func (us *Upstream) GetLine() int { - return us.Line -} diff --git a/backend/utils/nginx/components/upstream_server.go b/backend/utils/nginx/components/upstream_server.go deleted file mode 100644 index f5af46493..000000000 --- a/backend/utils/nginx/components/upstream_server.go +++ /dev/null @@ -1,83 +0,0 @@ -package components - -import ( - "fmt" - "sort" - "strings" -) - -type UpstreamServer struct { - Comment string - Address string - Flags []string - Parameters map[string]string - Line int -} - -func (uss *UpstreamServer) GetName() string { - return "server" -} - -func (uss *UpstreamServer) GetBlock() IBlock { - return nil -} - -func (uss *UpstreamServer) GetParameters() []string { - return uss.GetDirective().Parameters -} - -func (uss *UpstreamServer) GetComment() string { - return uss.Comment -} - -func (uss *UpstreamServer) GetDirective() *Directive { - directive := &Directive{ - Name: "server", - Parameters: make([]string, 0), - Block: nil, - } - - directive.Parameters = append(directive.Parameters, uss.Address) - - paramNames := make([]string, 0) - for k := range uss.Parameters { - paramNames = append(paramNames, k) - } - sort.Strings(paramNames) - - for _, k := range paramNames { - directive.Parameters = append(directive.Parameters, fmt.Sprintf("%s=%s", k, uss.Parameters[k])) - } - - directive.Parameters = append(directive.Parameters, uss.Flags...) - - return directive -} - -func NewUpstreamServer(directive IDirective) *UpstreamServer { - uss := &UpstreamServer{ - Comment: directive.GetComment(), - Flags: make([]string, 0), - Parameters: make(map[string]string, 0), - Line: directive.GetLine(), - } - - for i, parameter := range directive.GetParameters() { - if i == 0 { - uss.Address = parameter - continue - } - if strings.Contains(parameter, "=") { - s := strings.SplitN(parameter, "=", 2) - uss.Parameters[s[0]] = s[1] - } else { - uss.Flags = append(uss.Flags, parameter) - } - } - - return uss -} - -func (uss *UpstreamServer) GetLine() int { - return uss.Line -} diff --git a/backend/utils/nginx/dumper.go b/backend/utils/nginx/dumper.go deleted file mode 100644 index 06bc2bb15..000000000 --- a/backend/utils/nginx/dumper.go +++ /dev/null @@ -1,115 +0,0 @@ -package nginx - -import ( - "bytes" - "fmt" - "os" - "strings" - - "github.com/1Panel-dev/1Panel/backend/utils/nginx/components" -) - -var ( - IndentedStyle = &Style{ - SpaceBeforeBlocks: false, - StartIndent: 0, - Indent: 4, - } -) - -type Style struct { - SpaceBeforeBlocks bool - StartIndent int - Indent int -} - -func (s *Style) Iterate() *Style { - newStyle := &Style{ - SpaceBeforeBlocks: s.SpaceBeforeBlocks, - StartIndent: s.StartIndent + s.Indent, - Indent: s.Indent, - } - return newStyle -} - -func DumpDirective(d components.IDirective, style *Style) string { - var buf bytes.Buffer - - if style.SpaceBeforeBlocks && d.GetBlock() != nil { - buf.WriteString("\n") - } - buf.WriteString(fmt.Sprintf("%s%s", strings.Repeat(" ", style.StartIndent), d.GetName())) - if len(d.GetParameters()) > 0 { - buf.WriteString(fmt.Sprintf(" %s", strings.Join(d.GetParameters(), " "))) - } - if d.GetBlock() == nil { - if d.GetName() != "" { - buf.WriteRune(';') - buf.WriteString(" ") - } - if d.GetComment() != "" { - buf.WriteString(d.GetComment()) - } - } else { - buf.WriteString(" {") - if d.GetComment() != "" { - buf.WriteString(" ") - buf.WriteString(d.GetComment()) - } - buf.WriteString("\n") - buf.WriteString(DumpBlock(d.GetBlock(), style.Iterate(), d.GetBlock().GetLine())) - buf.WriteString(fmt.Sprintf("\n%s}", strings.Repeat(" ", style.StartIndent))) - } - return buf.String() -} - -func DumpBlock(b components.IBlock, style *Style, startLine int) string { - var buf bytes.Buffer - - if b.GetCodeBlock() != "" { - luaLines := strings.Split(b.GetCodeBlock(), "\n") - for i, line := range luaLines { - if strings.Replace(line, " ", "", -1) == "" { - continue - } - buf.WriteString(line) - if i != len(luaLines)-1 { - buf.WriteString("\n") - } - } - return buf.String() - } - - line := startLine - if b.GetLine() > startLine { - for i := 0; i < b.GetLine()-startLine; i++ { - buf.WriteString("\n") - } - line = b.GetLine() - } - - directives := b.GetDirectives() - for i, directive := range directives { - - if directive.GetLine() > line { - for i := 0; i < b.GetLine()-line; i++ { - buf.WriteString("\n") - } - line = b.GetLine() - } - - buf.WriteString(DumpDirective(directive, style)) - if i != len(directives)-1 { - buf.WriteString("\n") - } - } - return buf.String() -} - -func DumpConfig(c *components.Config, style *Style) string { - return DumpBlock(c.Block, style, 1) -} - -func WriteConfig(c *components.Config, style *Style) error { - return os.WriteFile(c.FilePath, []byte(DumpConfig(c, style)), 0644) -} diff --git a/backend/utils/nginx/parser/flag/flag.go b/backend/utils/nginx/parser/flag/flag.go deleted file mode 100644 index b39d9e7ac..000000000 --- a/backend/utils/nginx/parser/flag/flag.go +++ /dev/null @@ -1,59 +0,0 @@ -package flag - -type Type int - -const ( - EOF Type = iota - Eol - Keyword - QuotedString - Variable - BlockStart - BlockEnd - Semicolon - Comment - Illegal - Regex - LuaCode -) - -var ( - FlagName = map[Type]string{ - QuotedString: "QuotedString", - EOF: "Eof", - Keyword: "Keyword", - Variable: "Variable", - BlockStart: "BlockStart", - BlockEnd: "BlockEnd", - Semicolon: "Semicolon", - Comment: "Comment", - Illegal: "Illegal", - Regex: "Regex", - } -) - -func (tt Type) String() string { - return FlagName[tt] -} - -type Flag struct { - Type Type - Literal string - Line int - Column int -} - -func (t Flag) Lit(literal string) Flag { - t.Literal = literal - return t -} - -type Flags []Flag - -func (t Flag) Is(typ Type) bool { - return t.Type == typ -} - -func (t Flag) IsParameterEligible() bool { - return t.Is(Keyword) || t.Is(QuotedString) || t.Is(Variable) || t.Is(Regex) -} diff --git a/backend/utils/nginx/parser/lexer.go b/backend/utils/nginx/parser/lexer.go deleted file mode 100644 index fe64ee153..000000000 --- a/backend/utils/nginx/parser/lexer.go +++ /dev/null @@ -1,261 +0,0 @@ -package parser - -import ( - "bufio" - "bytes" - "github.com/1Panel-dev/1Panel/backend/utils/nginx/parser/flag" - "io" - "strings" -) - -type lexer struct { - reader *bufio.Reader - file string - line int - column int - inLuaBlock bool - Latest flag.Flag -} - -func lex(content string) *lexer { - return newLexer(bytes.NewBuffer([]byte(content))) -} - -func newLexer(r io.Reader) *lexer { - return &lexer{ - line: 1, - reader: bufio.NewReader(r), - } -} - -func (s *lexer) scan() flag.Flag { - s.Latest = s.getNextFlag() - return s.Latest -} - -//func (s *lexer) all() flag.Flags { -// tokens := make([]flag.Flag, 0) -// for { -// v := s.scan() -// if v.Type == flag.EOF || v.Type == -1 { -// break -// } -// tokens = append(tokens, v) -// } -// return tokens -//} - -func (s *lexer) getNextFlag() flag.Flag { - if s.inLuaBlock { - s.inLuaBlock = false - flag := s.scanLuaCode() - return flag - } -retoFlag: - ch := s.peek() - switch { - case isSpace(ch): - s.skipWhitespace() - goto retoFlag - case isEOF(ch): - return s.NewToken(flag.EOF).Lit(string(s.read())) - case ch == ';': - return s.NewToken(flag.Semicolon).Lit(string(s.read())) - case ch == '{': - if isLuaBlock(s.Latest) { - s.inLuaBlock = true - } - return s.NewToken(flag.BlockStart).Lit(string(s.read())) - case ch == '}': - return s.NewToken(flag.BlockEnd).Lit(string(s.read())) - case ch == '#': - return s.scanComment() - case ch == '$': - return s.scanVariable() - case isQuote(ch): - return s.scanQuotedString(ch) - default: - return s.scanKeyword() - } -} - -func (s *lexer) scanLuaCode() flag.Flag { - ret := s.NewToken(flag.LuaCode) - stack := make([]rune, 0, 50) - code := strings.Builder{} - - for { - ch := s.read() - if ch == rune(flag.EOF) { - panic("unexpected end of file while scanning a string, maybe an unclosed lua code?") - } - if ch == '#' { - code.WriteRune(ch) - code.WriteString(s.readUntil(isEndOfLine)) - continue - } else if ch == '}' { - if len(stack) == 0 { - _ = s.reader.UnreadRune() - return ret.Lit(strings.TrimRight(strings.Trim(code.String(), "\n"), "\n ")) - } - if stack[len(stack)-1] == '{' { - stack = stack[0 : len(stack)-1] - } - } else if ch == '{' { - stack = append(stack, ch) - } - code.WriteRune(ch) - } -} - -func (s *lexer) peek() rune { - r, _, _ := s.reader.ReadRune() - _ = s.reader.UnreadRune() - return r -} - -type runeCheck func(rune) bool - -func (s *lexer) readUntil(until runeCheck) string { - var buf bytes.Buffer - buf.WriteRune(s.read()) - - for { - if ch := s.peek(); isEOF(ch) { - break - } else if until(ch) { - break - } else { - buf.WriteRune(s.read()) - } - } - - return buf.String() -} - -func (s *lexer) NewToken(tokenType flag.Type) flag.Flag { - return flag.Flag{ - Type: tokenType, - Line: s.line, - Column: s.column, - } -} - -func (s *lexer) readWhile(while runeCheck) string { - var buf bytes.Buffer - buf.WriteRune(s.read()) - - for { - if ch := s.peek(); while(ch) { - buf.WriteRune(s.read()) - } else { - break - } - } - return buf.String() -} - -func (s *lexer) skipWhitespace() { - s.readWhile(isSpace) -} - -func (s *lexer) scanComment() flag.Flag { - return s.NewToken(flag.Comment).Lit(s.readUntil(isEndOfLine)) -} - -func (s *lexer) scanQuotedString(delimiter rune) flag.Flag { - var buf bytes.Buffer - tok := s.NewToken(flag.QuotedString) - _, _ = buf.WriteRune(s.read()) - for { - ch := s.read() - - if ch == rune(flag.EOF) { - panic("unexpected end of file while scanning a string, maybe an unclosed quote?") - } - - if ch == '\\' && (s.peek() == delimiter) { - buf.WriteRune(ch) - buf.WriteRune(s.read()) - continue - } - - _, _ = buf.WriteRune(ch) - if ch == delimiter { - break - } - } - - return tok.Lit(buf.String()) -} - -func (s *lexer) scanKeyword() flag.Flag { - var buf bytes.Buffer - tok := s.NewToken(flag.Keyword) - prev := s.read() - buf.WriteRune(prev) - for { - ch := s.peek() - - if isSpace(ch) || isEOF(ch) || ch == ';' { - break - } - - if ch == '{' { - if prev == '$' { - buf.WriteString(s.readUntil(func(r rune) bool { - return r == '}' - })) - buf.WriteRune(s.read()) //consume latest '}' - } else { - break - } - } - buf.WriteRune(s.read()) - } - - return tok.Lit(buf.String()) -} - -func (s *lexer) scanVariable() flag.Flag { - return s.NewToken(flag.Variable).Lit(s.readUntil(isKeywordTerminator)) -} - -func (s *lexer) read() rune { - ch, _, err := s.reader.ReadRune() - if err != nil { - return rune(flag.EOF) - } - - if ch == '\n' { - s.column = 1 - s.line++ - } else { - s.column++ - } - return ch -} - -func isQuote(ch rune) bool { - return ch == '"' || ch == '\'' || ch == '`' -} - -func isKeywordTerminator(ch rune) bool { - return isSpace(ch) || isEndOfLine(ch) || ch == '{' || ch == ';' -} - -func isSpace(ch rune) bool { - return ch == ' ' || ch == '\t' || isEndOfLine(ch) -} - -func isEOF(ch rune) bool { - return ch == rune(flag.EOF) -} - -func isEndOfLine(ch rune) bool { - return ch == '\r' || ch == '\n' -} - -func isLuaBlock(t flag.Flag) bool { - return t.Type == flag.Keyword && strings.HasSuffix(t.Literal, "_by_lua_block") -} diff --git a/backend/utils/nginx/parser/parser.go b/backend/utils/nginx/parser/parser.go deleted file mode 100644 index cd4de3dd6..000000000 --- a/backend/utils/nginx/parser/parser.go +++ /dev/null @@ -1,209 +0,0 @@ -package parser - -import ( - "bufio" - "errors" - "fmt" - components "github.com/1Panel-dev/1Panel/backend/utils/nginx/components" - "github.com/1Panel-dev/1Panel/backend/utils/nginx/parser/flag" - "os" - "strings" -) - -type Parser struct { - lexer *lexer - currentToken flag.Flag - followingToken flag.Flag - blockWrappers map[string]func(*components.Directive) (components.IDirective, error) - directiveWrappers map[string]func(*components.Directive) components.IDirective -} - -func NewStringParser(str string) *Parser { - return NewParserFromLexer(lex(str)) -} - -func NewParser(filePath string) (*Parser, error) { - f, err := os.Open(filePath) - if err != nil { - return nil, err - } - defer f.Close() - l := newLexer(bufio.NewReader(f)) - l.file = filePath - p := NewParserFromLexer(l) - return p, nil -} - -func NewParserFromLexer(lexer *lexer) *Parser { - parser := &Parser{ - lexer: lexer, - } - - parser.nextToken() - parser.nextToken() - - parser.blockWrappers = map[string]func(*components.Directive) (components.IDirective, error){ - "http": func(directive *components.Directive) (components.IDirective, error) { - return parser.wrapHttp(directive), nil - }, - "server": func(directive *components.Directive) (components.IDirective, error) { - return parser.wrapServer(directive), nil - }, - "location": func(directive *components.Directive) (components.IDirective, error) { - return parser.wrapLocation(directive), nil - }, - "upstream": func(directive *components.Directive) (components.IDirective, error) { - return parser.wrapUpstream(directive), nil - }, - "_by_lua_block": func(directive *components.Directive) (components.IDirective, error) { - return parser.wrapLuaBlock(directive) - }, - } - - parser.directiveWrappers = map[string]func(*components.Directive) components.IDirective{ - "server": func(directive *components.Directive) components.IDirective { - return parser.parseUpstreamServer(directive) - }, - } - - return parser -} - -func (p *Parser) nextToken() { - p.currentToken = p.followingToken - p.followingToken = p.lexer.scan() -} - -func (p *Parser) curTokenIs(t flag.Type) bool { - return p.currentToken.Type == t -} - -func (p *Parser) followingTokenIs(t flag.Type) bool { - return p.followingToken.Type == t -} - -func (p *Parser) Parse() (*components.Config, error) { - parsedBlock, err := p.parseBlock(false) - if err != nil { - return nil, err - } - c := &components.Config{ - FilePath: p.lexer.file, - Block: parsedBlock, - } - return c, err -} - -func (p *Parser) parseBlock(inBlock bool) (*components.Block, error) { - context := &components.Block{ - Comment: "", - Directives: make([]components.IDirective, 0), - Line: p.currentToken.Line, - } - -parsingloop: - for { - switch { - case p.curTokenIs(flag.EOF): - if inBlock { - return nil, errors.New("unexpected eof in block") - } - break parsingloop - case p.curTokenIs(flag.BlockEnd): - break parsingloop - case p.curTokenIs(flag.LuaCode): - context.IsLuaBlock = true - context.LiteralCode = p.currentToken.Literal - case p.curTokenIs(flag.Keyword) || p.curTokenIs(flag.QuotedString): - s, err := p.parseStatement() - if err != nil { - return nil, err - } - context.Directives = append(context.Directives, s) - case p.curTokenIs(flag.Comment): - context.Directives = append(context.Directives, &components.Comment{ - Detail: p.currentToken.Literal, - Line: p.currentToken.Line, - }) - } - p.nextToken() - } - - return context, nil -} - -func (p *Parser) parseStatement() (components.IDirective, error) { - d := &components.Directive{ - Name: p.currentToken.Literal, - Line: p.currentToken.Line, - } - - for p.nextToken(); p.currentToken.IsParameterEligible(); p.nextToken() { - d.Parameters = append(d.Parameters, p.currentToken.Literal) - } - - if p.curTokenIs(flag.Semicolon) { - if dw, ok := p.directiveWrappers[d.Name]; ok { - return dw(d), nil - } - if p.followingTokenIs(flag.Comment) && p.currentToken.Line == p.followingToken.Line { - d.Comment = p.followingToken.Literal - p.nextToken() - } - return d, nil - } - - if p.curTokenIs(flag.BlockStart) { - inLineComment := "" - if p.followingTokenIs(flag.Comment) && p.currentToken.Line == p.followingToken.Line { - inLineComment = p.followingToken.Literal - p.nextToken() - p.nextToken() - } - block, err := p.parseBlock(false) - if err != nil { - return nil, err - } - - block.Comment = inLineComment - d.Block = block - - if strings.HasSuffix(d.Name, "_by_lua_block") { - return p.blockWrappers["_by_lua_block"](d) - } - - if bw, ok := p.blockWrappers[d.Name]; ok { - return bw(d) - } - return d, nil - } - - panic(fmt.Errorf("unexpected token %s (%s) on line %d, column %d", p.currentToken.Type.String(), p.currentToken.Literal, p.currentToken.Line, p.currentToken.Column)) -} - -func (p *Parser) wrapLocation(directive *components.Directive) *components.Location { - return components.NewLocation(directive) -} - -func (p *Parser) wrapServer(directive *components.Directive) *components.Server { - s, _ := components.NewServer(directive) - return s -} - -func (p *Parser) wrapUpstream(directive *components.Directive) *components.Upstream { - s, _ := components.NewUpstream(directive) - return s -} - -func (p *Parser) wrapHttp(directive *components.Directive) *components.Http { - h, _ := components.NewHttp(directive) - return h -} - -func (p *Parser) wrapLuaBlock(directive *components.Directive) (*components.LuaBlock, error) { - return components.NewLuaBlock(directive) -} - -func (p *Parser) parseUpstreamServer(directive *components.Directive) *components.UpstreamServer { - return components.NewUpstreamServer(directive) -} diff --git a/backend/utils/ntp/ntp.go b/backend/utils/ntp/ntp.go deleted file mode 100644 index 920e1eae1..000000000 --- a/backend/utils/ntp/ntp.go +++ /dev/null @@ -1,84 +0,0 @@ -package ntp - -import ( - "encoding/binary" - "fmt" - "net" - "runtime" - "time" - - "github.com/1Panel-dev/1Panel/backend/utils/cmd" -) - -const ntpEpochOffset = 2208988800 - -type packet struct { - Settings uint8 - Stratum uint8 - Poll int8 - Precision int8 - RootDelay uint32 - RootDispersion uint32 - ReferenceID uint32 - RefTimeSec uint32 - RefTimeFrac uint32 - OrigTimeSec uint32 - OrigTimeFrac uint32 - RxTimeSec uint32 - RxTimeFrac uint32 - TxTimeSec uint32 - TxTimeFrac uint32 -} - -func GetRemoteTime(site string) (time.Time, error) { - conn, err := net.Dial("udp", site+":123") - if err != nil { - return time.Time{}, fmt.Errorf("failed to connect: %v", err) - } - defer conn.Close() - if err := conn.SetDeadline(time.Now().Add(15 * time.Second)); err != nil { - return time.Time{}, fmt.Errorf("failed to set deadline: %v", err) - } - - req := &packet{Settings: 0x1B} - - if err := binary.Write(conn, binary.BigEndian, req); err != nil { - return time.Time{}, fmt.Errorf("failed to set request: %v", err) - } - - rsp := &packet{} - if err := binary.Read(conn, binary.BigEndian, rsp); err != nil { - return time.Time{}, fmt.Errorf("failed to read server response: %v", err) - } - - secs := float64(rsp.TxTimeSec) - ntpEpochOffset - nanos := (int64(rsp.TxTimeFrac) * 1e9) >> 32 - - showtime := time.Unix(int64(secs), nanos) - - return showtime, nil -} - -func UpdateSystemTime(dateTime string) error { - system := runtime.GOOS - if system == "linux" { - stdout2, err := cmd.Execf(`%s date -s "%s"`, cmd.SudoHandleCmd(), dateTime) - if err != nil { - return fmt.Errorf("update system time failed,stdout: %s, err: %v", stdout2, err) - } - return nil - } - return fmt.Errorf("the current system architecture %v does not support synchronization", system) -} - -func UpdateSystemTimeZone(timezone string) error { - system := runtime.GOOS - if system == "linux" { - stdout, err := cmd.Execf(`%s timedatectl set-timezone "%s"`, cmd.SudoHandleCmd(), timezone) - if err != nil { - return fmt.Errorf("update system time zone failed, stdout: %s, err: %v", stdout, err) - } - return nil - } - return fmt.Errorf("the current system architecture %v does not support synchronization", system) -} diff --git a/backend/utils/postgresql/client.go b/backend/utils/postgresql/client.go deleted file mode 100644 index 2122c28bb..000000000 --- a/backend/utils/postgresql/client.go +++ /dev/null @@ -1,59 +0,0 @@ -package postgresql - -import ( - "context" - "database/sql" - "errors" - "fmt" - "time" - - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/postgresql/client" - _ "github.com/jackc/pgx/v5/stdlib" -) - -type PostgresqlClient interface { - Create(info client.CreateInfo) error - CreateUser(info client.CreateInfo, withDeleteDB bool) error - Delete(info client.DeleteInfo) error - ChangePrivileges(info client.Privileges) error - ChangePassword(info client.PasswordChangeInfo) error - - Backup(info client.BackupInfo) error - Recover(info client.RecoverInfo) error - SyncDB() ([]client.SyncDBInfo, error) - Close() -} - -func NewPostgresqlClient(conn client.DBInfo) (PostgresqlClient, error) { - if conn.From == "local" { - connArgs := []string{"exec", conn.Address, "psql", "-t", "-U", conn.Username, "-c"} - return client.NewLocal(connArgs, conn.Address, conn.Username, conn.Password, conn.Database), nil - } - - connArgs := fmt.Sprintf("postgres://%s:%s@%s:%d/?sslmode=disable", conn.Username, conn.Password, conn.Address, conn.Port) - db, err := sql.Open("pgx", connArgs) - if err != nil { - return nil, err - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(conn.Timeout)*time.Second) - defer cancel() - if err := db.PingContext(ctx); err != nil { - return nil, err - } - if errors.Is(ctx.Err(), context.DeadlineExceeded) { - return nil, buserr.New(constant.ErrExecTimeOut) - } - - return client.NewRemote(client.Remote{ - Client: db, - From: "remote", - Database: conn.Database, - User: conn.Username, - Password: conn.Password, - Address: conn.Address, - Port: conn.Port, - }), nil -} diff --git a/backend/utils/postgresql/client/info.go b/backend/utils/postgresql/client/info.go deleted file mode 100644 index b1b789dcf..000000000 --- a/backend/utils/postgresql/client/info.go +++ /dev/null @@ -1,81 +0,0 @@ -package client - -import ( - _ "github.com/jackc/pgx/v5/stdlib" -) - -type DBInfo struct { - From string `json:"from"` - Database string `json:"database"` - Address string `json:"address"` - Port uint `json:"port"` - Username string `json:"userName"` - Password string `json:"password"` - - Timeout uint `json:"timeout"` // second -} - -type CreateInfo struct { - Name string `json:"name"` - Username string `json:"userName"` - Password string `json:"password"` - SuperUser bool `json:"superUser"` - - Timeout uint `json:"timeout"` // second -} - -type Privileges struct { - Username string `json:"userName"` - SuperUser bool `json:"superUser"` - - Timeout uint `json:"timeout"` // second -} - -type DeleteInfo struct { - Name string `json:"name"` - Username string `json:"userName"` - - ForceDelete bool `json:"forceDelete"` - Timeout uint `json:"timeout"` // second -} - -type PasswordChangeInfo struct { - Username string `json:"userName"` - Password string `json:"password"` - - Timeout uint `json:"timeout"` // second -} - -type BackupInfo struct { - Name string `json:"name"` - TargetDir string `json:"targetDir"` - FileName string `json:"fileName"` - - Timeout uint `json:"timeout"` // second -} - -type RecoverInfo struct { - Name string `json:"name"` - SourceFile string `json:"sourceFile"` - Username string `json:"username"` - - Timeout uint `json:"timeout"` // second -} - -type SyncDBInfo struct { - Name string `json:"name"` - From string `json:"from"` - PostgresqlName string `json:"postgresqlName"` -} -type Status struct { - Uptime string `json:"uptime"` - Version string `json:"version"` - MaxConnections string `json:"max_connections"` - Autovacuum string `json:"autovacuum"` - CurrentConnections string `json:"current_connections"` - HitRatio string `json:"hit_ratio"` - SharedBuffers string `json:"shared_buffers"` - BuffersClean string `json:"buffers_clean"` - MaxwrittenClean string `json:"maxwritten_clean"` - BuffersBackendFsync string `json:"buffers_backend_fsync"` -} diff --git a/backend/utils/postgresql/client/local.go b/backend/utils/postgresql/client/local.go deleted file mode 100644 index b36fe6608..000000000 --- a/backend/utils/postgresql/client/local.go +++ /dev/null @@ -1,230 +0,0 @@ -package client - -import ( - "bytes" - "compress/gzip" - "context" - "errors" - "fmt" - "os" - "os/exec" - "path" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/files" -) - -type Local struct { - PrefixCommand []string - Database string - Username string - Password string - ContainerName string -} - -func NewLocal(command []string, containerName, username, password, database string) *Local { - return &Local{PrefixCommand: command, ContainerName: containerName, Username: username, Password: password, Database: database} -} - -func (r *Local) Create(info CreateInfo) error { - createSql := fmt.Sprintf("CREATE DATABASE \"%s\"", info.Name) - if err := r.ExecSQL(createSql, info.Timeout); err != nil { - if strings.Contains(strings.ToLower(err.Error()), "already exists") { - return buserr.New(constant.ErrDatabaseIsExist) - } - return err - } - - if err := r.CreateUser(info, true); err != nil { - _ = r.ExecSQL(fmt.Sprintf("DROP DATABASE \"%s\"", info.Name), info.Timeout) - return err - } - - return nil -} - -func (r *Local) ChangePrivileges(info Privileges) error { - super := "SUPERUSER" - if !info.SuperUser { - super = "NOSUPERUSER" - } - changeSql := fmt.Sprintf("ALTER USER \"%s\" WITH %s", info.Username, super) - return r.ExecSQL(changeSql, info.Timeout) -} - -func (r *Local) CreateUser(info CreateInfo, withDeleteDB bool) error { - createSql := fmt.Sprintf("CREATE USER \"%s\" WITH PASSWORD '%s'", info.Username, info.Password) - if err := r.ExecSQL(createSql, info.Timeout); err != nil { - if strings.Contains(strings.ToLower(err.Error()), "already exists") { - return buserr.New(constant.ErrUserIsExist) - } - if withDeleteDB { - _ = r.Delete(DeleteInfo{ - Name: info.Name, - Username: info.Username, - ForceDelete: true, - Timeout: 300}) - } - return err - } - if info.SuperUser { - if err := r.ChangePrivileges(Privileges{SuperUser: true, Username: info.Username, Timeout: info.Timeout}); err != nil { - if withDeleteDB { - _ = r.Delete(DeleteInfo{ - Name: info.Name, - Username: info.Username, - ForceDelete: true, - Timeout: 300}) - } - return err - } - } - grantStr := fmt.Sprintf("GRANT ALL PRIVILEGES ON DATABASE \"%s\" TO \"%s\"", info.Name, info.Username) - if err := r.ExecSQL(grantStr, info.Timeout); err != nil { - if withDeleteDB { - _ = r.Delete(DeleteInfo{ - Name: info.Name, - Username: info.Username, - ForceDelete: true, - Timeout: 300}) - } - return err - } - return nil -} - -func (r *Local) Delete(info DeleteInfo) error { - if len(info.Name) != 0 { - dropSql := fmt.Sprintf("DROP DATABASE \"%s\"", info.Name) - if err := r.ExecSQL(dropSql, info.Timeout); err != nil && !info.ForceDelete { - return err - } - } - dropSql := fmt.Sprintf("DROP USER \"%s\"", info.Username) - if err := r.ExecSQL(dropSql, info.Timeout); err != nil && !info.ForceDelete { - if strings.Contains(strings.ToLower(err.Error()), "depend on it") { - return buserr.WithDetail(constant.ErrInUsed, info.Username, nil) - } - return err - } - return nil -} - -func (r *Local) ChangePassword(info PasswordChangeInfo) error { - changeSql := fmt.Sprintf("ALTER USER \"%s\" WITH PASSWORD '%s'", info.Username, info.Password) - if err := r.ExecSQL(changeSql, info.Timeout); err != nil { - return err - } - - return nil -} - -func (r *Local) Backup(info BackupInfo) error { - fileOp := files.NewFileOp() - if !fileOp.Stat(info.TargetDir) { - if err := os.MkdirAll(info.TargetDir, os.ModePerm); err != nil { - return fmt.Errorf("mkdir %s failed, err: %v", info.TargetDir, err) - } - } - outfile, err := os.OpenFile(path.Join(info.TargetDir, info.FileName), os.O_RDWR|os.O_CREATE, 0755) - if err != nil { - return fmt.Errorf("open file %s failed, err: %v", path.Join(info.TargetDir, info.FileName), err) - } - defer outfile.Close() - global.LOG.Infof("start to pg_dump | gzip > %s.gzip", info.TargetDir+"/"+info.FileName) - cmd := exec.Command("docker", "exec", r.ContainerName, "pg_dump", "-F", "c", "-U", r.Username, "-d", info.Name) - var stderr bytes.Buffer - cmd.Stderr = &stderr - - gzipCmd := exec.Command("gzip", "-cf") - gzipCmd.Stdin, _ = cmd.StdoutPipe() - gzipCmd.Stdout = outfile - _ = gzipCmd.Start() - - if err := cmd.Run(); err != nil { - return fmt.Errorf("handle backup database failed, err: %v", stderr.String()) - } - _ = gzipCmd.Wait() - return nil -} - -func (r *Local) Recover(info RecoverInfo) error { - fi, _ := os.Open(info.SourceFile) - defer fi.Close() - cmd := exec.Command("docker", "exec", "-i", r.ContainerName, "pg_restore", "-F", "c", "-c", "-U", r.Username, "-d", info.Name) - if strings.HasSuffix(info.SourceFile, ".gz") { - gzipFile, err := os.Open(info.SourceFile) - if err != nil { - return err - } - defer gzipFile.Close() - gzipReader, err := gzip.NewReader(gzipFile) - if err != nil { - return err - } - defer gzipReader.Close() - cmd.Stdin = gzipReader - } else { - cmd.Stdin = fi - } - stdout, err := cmd.CombinedOutput() - if err != nil || strings.HasPrefix(string(stdout), "ERROR ") { - return errors.New(string(stdout)) - } - - return nil -} - -func (r *Local) SyncDB() ([]SyncDBInfo, error) { - var datas []SyncDBInfo - lines, err := r.ExecSQLForRows("SELECT datname FROM pg_database", 300) - if err != nil { - return datas, err - } - for _, line := range lines { - itemLine := strings.TrimLeft(line, " ") - if len(itemLine) == 0 || itemLine == "postgres" || itemLine == "template1" || itemLine == "template0" || itemLine == r.Username { - continue - } - datas = append(datas, SyncDBInfo{Name: itemLine, From: "local", PostgresqlName: r.Database}) - } - return datas, nil -} - -func (r *Local) Close() {} - -func (r *Local) ExecSQL(command string, timeout uint) error { - itemCommand := r.PrefixCommand[:] - itemCommand = append(itemCommand, command) - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) - defer cancel() - cmd := exec.CommandContext(ctx, "docker", itemCommand...) - stdout, err := cmd.CombinedOutput() - if errors.Is(ctx.Err(), context.DeadlineExceeded) { - return buserr.New(constant.ErrExecTimeOut) - } - if err != nil || strings.HasPrefix(string(stdout), "ERROR ") { - return errors.New(string(stdout)) - } - return nil -} - -func (r *Local) ExecSQLForRows(command string, timeout uint) ([]string, error) { - itemCommand := r.PrefixCommand[:] - itemCommand = append(itemCommand, command) - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) - defer cancel() - cmd := exec.CommandContext(ctx, "docker", itemCommand...) - stdout, err := cmd.CombinedOutput() - if errors.Is(ctx.Err(), context.DeadlineExceeded) { - return nil, buserr.New(constant.ErrExecTimeOut) - } - if err != nil || strings.HasPrefix(string(stdout), "ERROR ") { - return nil, errors.New(string(stdout)) - } - return strings.Split(string(stdout), "\n"), nil -} diff --git a/backend/utils/postgresql/client/remote.go b/backend/utils/postgresql/client/remote.go deleted file mode 100644 index b4a954181..000000000 --- a/backend/utils/postgresql/client/remote.go +++ /dev/null @@ -1,311 +0,0 @@ -package client - -import ( - "bufio" - "context" - "database/sql" - "fmt" - "io" - "os" - "os/exec" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/docker/docker/api/types/image" - "github.com/pkg/errors" - - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/utils/docker" - "github.com/1Panel-dev/1Panel/backend/utils/files" - _ "github.com/jackc/pgx/v5/stdlib" -) - -type Remote struct { - Client *sql.DB - From string - Database string - User string - Password string - Address string - Port uint -} - -func NewRemote(db Remote) *Remote { - return &db -} -func (r *Remote) Create(info CreateInfo) error { - createSql := fmt.Sprintf("CREATE DATABASE \"%s\"", info.Name) - if err := r.ExecSQL(createSql, info.Timeout); err != nil { - if strings.Contains(strings.ToLower(err.Error()), "already exists") { - return buserr.New(constant.ErrDatabaseIsExist) - } - return err - } - if err := r.CreateUser(info, true); err != nil { - return err - } - return nil -} - -func (r *Remote) CreateUser(info CreateInfo, withDeleteDB bool) error { - createSql := fmt.Sprintf("CREATE USER \"%s\" WITH PASSWORD '%s'", info.Username, info.Password) - if err := r.ExecSQL(createSql, info.Timeout); err != nil { - if strings.Contains(strings.ToLower(err.Error()), "already exists") { - return buserr.New(constant.ErrUserIsExist) - } - if withDeleteDB { - _ = r.Delete(DeleteInfo{ - Name: info.Name, - Username: info.Username, - ForceDelete: true, - Timeout: 300}) - } - return err - } - if info.SuperUser { - if err := r.ChangePrivileges(Privileges{SuperUser: true, Username: info.Username, Timeout: info.Timeout}); err != nil { - if withDeleteDB { - _ = r.Delete(DeleteInfo{ - Name: info.Name, - Username: info.Username, - ForceDelete: true, - Timeout: 300}) - } - return err - } - } - grantSql := fmt.Sprintf("GRANT ALL PRIVILEGES ON DATABASE \"%s\" TO \"%s\"", info.Name, info.Username) - if err := r.ExecSQL(grantSql, info.Timeout); err != nil { - if withDeleteDB { - _ = r.Delete(DeleteInfo{ - Name: info.Name, - Username: info.Username, - ForceDelete: true, - Timeout: 300}) - } - return err - } - - return nil -} - -func (r *Remote) Delete(info DeleteInfo) error { - if len(info.Name) != 0 { - dropSql := fmt.Sprintf("DROP DATABASE \"%s\"", info.Name) - if err := r.ExecSQL(dropSql, info.Timeout); err != nil && !info.ForceDelete { - return err - } - } - dropSql := fmt.Sprintf("DROP USER \"%s\"", info.Username) - if err := r.ExecSQL(dropSql, info.Timeout); err != nil && !info.ForceDelete { - if strings.Contains(strings.ToLower(err.Error()), "depend on it") { - return buserr.WithDetail(constant.ErrInUsed, info.Username, nil) - } - return err - } - return nil -} - -func (r *Remote) ChangePrivileges(info Privileges) error { - super := "SUPERUSER" - if !info.SuperUser { - super = "NOSUPERUSER" - } - return r.ExecSQL(fmt.Sprintf("ALTER USER \"%s\" WITH %s", info.Username, super), info.Timeout) -} - -func (r *Remote) ChangePassword(info PasswordChangeInfo) error { - return r.ExecSQL(fmt.Sprintf("ALTER USER \"%s\" WITH ENCRYPTED PASSWORD '%s'", info.Username, info.Password), info.Timeout) -} - -func (r *Remote) Backup(info BackupInfo) error { - imageTag, err := loadImageTag() - if err != nil { - return err - } - fileOp := files.NewFileOp() - if !fileOp.Stat(info.TargetDir) { - if err := os.MkdirAll(info.TargetDir, os.ModePerm); err != nil { - return fmt.Errorf("mkdir %s failed, err: %v", info.TargetDir, err) - } - } - fileNameItem := info.TargetDir + "/" + strings.TrimSuffix(info.FileName, ".gz") - backupCommand := exec.Command("bash", "-c", - fmt.Sprintf("docker run --rm --net=host -i %s /bin/bash -c 'PGPASSWORD=%s pg_dump -h %s -p %d --no-owner -Fc -U %s %s' > %s", - imageTag, r.Password, r.Address, r.Port, r.User, info.Name, fileNameItem)) - _ = backupCommand.Run() - b := make([]byte, 5) - n := []byte{80, 71, 68, 77, 80} - handle, err := os.OpenFile(fileNameItem, os.O_RDONLY, os.ModePerm) - if err != nil { - return fmt.Errorf("backup file not found,err:%v", err) - } - defer handle.Close() - _, _ = handle.Read(b) - if string(b) != string(n) { - errBytes, _ := os.ReadFile(fileNameItem) - return fmt.Errorf("backup failed,err:%s", string(errBytes)) - } - - gzipCmd := exec.Command("gzip", fileNameItem) - stdout, err := gzipCmd.CombinedOutput() - if err != nil { - return fmt.Errorf("gzip file %s failed, stdout: %v, err: %v", strings.TrimSuffix(info.FileName, ".gz"), string(stdout), err) - } - return nil -} - -func (r *Remote) Recover(info RecoverInfo) error { - imageTag, err := loadImageTag() - if err != nil { - return err - } - fileName := info.SourceFile - if strings.HasSuffix(info.SourceFile, ".sql.gz") { - fileName = strings.TrimSuffix(info.SourceFile, ".gz") - gzipCmd := exec.Command("gunzip", info.SourceFile) - stdout, err := gzipCmd.CombinedOutput() - if err != nil { - return fmt.Errorf("gunzip file %s failed, stdout: %v, err: %v", info.SourceFile, string(stdout), err) - } - defer func() { - gzipCmd := exec.Command("gzip", fileName) - _, _ = gzipCmd.CombinedOutput() - }() - } - recoverCommand := exec.Command("bash", "-c", - fmt.Sprintf("docker run --rm --net=host -i %s /bin/bash -c 'PGPASSWORD=%s pg_restore -h %s -p %d --verbose --clean --no-privileges --no-owner -Fc -U %s -d %s --role=%s' < %s", - imageTag, r.Password, r.Address, r.Port, r.User, info.Name, info.Username, fileName)) - pipe, _ := recoverCommand.StdoutPipe() - stderrPipe, _ := recoverCommand.StderrPipe() - defer pipe.Close() - defer stderrPipe.Close() - if err := recoverCommand.Start(); err != nil { - return err - } - reader := bufio.NewReader(pipe) - for { - readString, err := reader.ReadString('\n') - if errors.Is(err, io.EOF) { - break - } - if err != nil { - all, _ := io.ReadAll(stderrPipe) - global.LOG.Errorf("[Postgresql] DB:[%s] Recover Error: %s", info.Name, string(all)) - return err - } - global.LOG.Infof("[Postgresql] DB:[%s] Restoring: %s", info.Name, readString) - } - - return nil -} - -func (r *Remote) SyncDB() ([]SyncDBInfo, error) { - ctx, cancel := context.WithTimeout(context.Background(), 300*time.Second) - defer cancel() - - var datas []SyncDBInfo - rows, err := r.Client.Query("SELECT datname FROM pg_database;") - if err != nil { - return nil, err - } - defer rows.Close() - for rows.Next() { - var dbName string - if err := rows.Scan(&dbName); err != nil { - continue - } - if len(dbName) == 0 || dbName == "postgres" || dbName == "template1" || dbName == "template0" || dbName == r.User { - continue - } - datas = append(datas, SyncDBInfo{Name: dbName, From: r.From, PostgresqlName: r.Database}) - } - if errors.Is(ctx.Err(), context.DeadlineExceeded) { - return nil, buserr.New(constant.ErrExecTimeOut) - } - return datas, nil -} - -func (r *Remote) Close() { - _ = r.Client.Close() -} - -func (r *Remote) ExecSQL(command string, timeout uint) error { - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) - defer cancel() - - if _, err := r.Client.ExecContext(ctx, command); err != nil { - return err - } - if errors.Is(ctx.Err(), context.DeadlineExceeded) { - return buserr.New(constant.ErrExecTimeOut) - } - - return nil -} - -func loadImageTag() (string, error) { - var ( - app model.App - appDetails []model.AppDetail - versions []string - ) - if err := global.DB.Where("key = ?", "postgresql").First(&app).Error; err != nil { - versions = []string{"postgres:16.1-alpine", "postgres:16.0-alpine"} - } else { - if err := global.DB.Where("app_id = ?", app.ID).Find(&appDetails).Error; err != nil { - versions = []string{"postgres:16.1-alpine", "postgres:16.0-alpine"} - } else { - for _, item := range appDetails { - versions = append(versions, "postgres:"+item.Version) - } - } - } - - client, err := docker.NewDockerClient() - if err != nil { - return "", err - } - defer client.Close() - images, err := client.ImageList(context.Background(), image.ListOptions{}) - if err != nil { - return "", err - } - - itemTag := "" - for _, item := range versions { - for _, image := range images { - for _, tag := range image.RepoTags { - if tag == item { - itemTag = tag - break - } - } - if len(itemTag) != 0 { - break - } - } - if len(itemTag) != 0 { - break - } - } - if len(itemTag) != 0 { - return itemTag, nil - } - - itemTag = "postgres:16.1-alpine" - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) - defer cancel() - if _, err := client.ImagePull(ctx, itemTag, image.PullOptions{}); err != nil { - if errors.Is(ctx.Err(), context.DeadlineExceeded) { - return itemTag, buserr.New(constant.ErrPgImagePull) - } - global.LOG.Errorf("image %s pull failed, err: %v", itemTag, err) - return itemTag, fmt.Errorf("image %s pull failed, err: %v", itemTag, err) - } - - return itemTag, nil -} diff --git a/backend/utils/qqwry/qqwry.go b/backend/utils/qqwry/qqwry.go deleted file mode 100644 index bd2b3d46a..000000000 --- a/backend/utils/qqwry/qqwry.go +++ /dev/null @@ -1,165 +0,0 @@ -package qqwry - -import ( - "encoding/binary" - "net" - "strings" - - "github.com/1Panel-dev/1Panel/cmd/server/qqwry" - "golang.org/x/text/encoding/simplifiedchinese" -) - -const ( - indexLen = 7 - redirectMode1 = 0x01 - redirectMode2 = 0x02 -) - -var IpCommonDictionary []byte - -type QQwry struct { - Data []byte - Offset int64 -} - -func NewQQwry() (*QQwry, error) { - IpCommonDictionary := qqwry.QQwryByte - return &QQwry{Data: IpCommonDictionary}, nil -} - -// readData 从文件中读取数据 -func (q *QQwry) readData(num int, offset ...int64) (rs []byte) { - if len(offset) > 0 { - q.setOffset(offset[0]) - } - nums := int64(num) - end := q.Offset + nums - dataNum := int64(len(q.Data)) - if q.Offset > dataNum { - return nil - } - - if end > dataNum { - end = dataNum - } - rs = q.Data[q.Offset:end] - q.Offset = end - return -} - -// setOffset 设置偏移量 -func (q *QQwry) setOffset(offset int64) { - q.Offset = offset -} - -// Find ip地址查询对应归属地信息 -func (q *QQwry) Find(ip string) (res ResultQQwry) { - res = ResultQQwry{} - res.IP = ip - if strings.Count(ip, ".") != 3 { - return res - } - offset := q.searchIndex(binary.BigEndian.Uint32(net.ParseIP(ip).To4())) - if offset <= 0 { - return - } - - var area []byte - mode := q.readMode(offset + 4) - if mode == redirectMode1 { - countryOffset := q.readUInt24() - mode = q.readMode(countryOffset) - if mode == redirectMode2 { - c := q.readUInt24() - area = q.readString(c) - } else { - area = q.readString(countryOffset) - } - } else if mode == redirectMode2 { - countryOffset := q.readUInt24() - area = q.readString(countryOffset) - } else { - area = q.readString(offset + 4) - } - - enc := simplifiedchinese.GBK.NewDecoder() - res.Area, _ = enc.String(string(area)) - - return -} - -type ResultQQwry struct { - IP string `json:"ip"` - Area string `json:"area"` -} - -// readMode 获取偏移值类型 -func (q *QQwry) readMode(offset uint32) byte { - mode := q.readData(1, int64(offset)) - return mode[0] -} - -// readString 获取字符串 -func (q *QQwry) readString(offset uint32) []byte { - q.setOffset(int64(offset)) - data := make([]byte, 0, 30) - for { - buf := q.readData(1) - if buf[0] == 0 { - break - } - data = append(data, buf[0]) - } - return data -} - -// searchIndex 查找索引位置 -func (q *QQwry) searchIndex(ip uint32) uint32 { - header := q.readData(8, 0) - - start := binary.LittleEndian.Uint32(header[:4]) - end := binary.LittleEndian.Uint32(header[4:]) - - for { - mid := q.getMiddleOffset(start, end) - buf := q.readData(indexLen, int64(mid)) - _ip := binary.LittleEndian.Uint32(buf[:4]) - - if end-start == indexLen { - offset := byteToUInt32(buf[4:]) - buf = q.readData(indexLen) - if ip < binary.LittleEndian.Uint32(buf[:4]) { - return offset - } - return 0 - } - - if _ip > ip { - end = mid - } else if _ip < ip { - start = mid - } else if _ip == ip { - return byteToUInt32(buf[4:]) - } - } -} - -// readUInt24 -func (q *QQwry) readUInt24() uint32 { - buf := q.readData(3) - return byteToUInt32(buf) -} - -// getMiddleOffset -func (q *QQwry) getMiddleOffset(start uint32, end uint32) uint32 { - records := ((end - start) / indexLen) >> 1 - return start + records*indexLen -} - -// byteToUInt32 将 byte 转换为uint32 -func byteToUInt32(data []byte) uint32 { - i := uint32(data[0]) & 0xff - i |= (uint32(data[1]) << 8) & 0xff00 - i |= (uint32(data[2]) << 16) & 0xff0000 - return i -} diff --git a/backend/utils/redis/redis.go b/backend/utils/redis/redis.go deleted file mode 100644 index c30053f6a..000000000 --- a/backend/utils/redis/redis.go +++ /dev/null @@ -1,28 +0,0 @@ -package redis - -import ( - "fmt" - - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/go-redis/redis" -) - -type DBInfo struct { - Address string `json:"address"` - Port uint `json:"port"` - Password string `json:"password"` -} - -func NewRedisClient(conn DBInfo) (*redis.Client, error) { - client := redis.NewClient(&redis.Options{ - Addr: fmt.Sprintf("%s:%v", conn.Address, conn.Port), - Password: conn.Password, - DB: 0, - }) - - if _, err := client.Ping().Result(); err != nil { - global.LOG.Errorf("check redis conn failed, err: %v", err) - return client, err - } - return client, nil -} diff --git a/backend/utils/ssh/ssh.go b/backend/utils/ssh/ssh.go deleted file mode 100644 index f487182d4..000000000 --- a/backend/utils/ssh/ssh.go +++ /dev/null @@ -1,142 +0,0 @@ -package ssh - -import ( - "bytes" - "fmt" - "io" - "strings" - "sync" - "time" - - gossh "golang.org/x/crypto/ssh" -) - -type ConnInfo struct { - User string `json:"user"` - Addr string `json:"addr"` - Port int `json:"port"` - AuthMode string `json:"authMode"` - Password string `json:"password"` - PrivateKey []byte `json:"privateKey"` - PassPhrase []byte `json:"passPhrase"` - DialTimeOut time.Duration `json:"dialTimeOut"` - - Client *gossh.Client `json:"client"` - Session *gossh.Session `json:"session"` - LastResult string `json:"lastResult"` -} - -func (c *ConnInfo) NewClient() (*ConnInfo, error) { - if strings.Contains(c.Addr, ":") { - c.Addr = fmt.Sprintf("[%s]", c.Addr) - } - config := &gossh.ClientConfig{} - config.SetDefaults() - addr := fmt.Sprintf("%s:%d", c.Addr, c.Port) - config.User = c.User - if c.AuthMode == "password" { - config.Auth = []gossh.AuthMethod{gossh.Password(c.Password)} - } else { - signer, err := makePrivateKeySigner(c.PrivateKey, c.PassPhrase) - if err != nil { - return nil, err - } - config.Auth = []gossh.AuthMethod{gossh.PublicKeys(signer)} - } - if c.DialTimeOut == 0 { - c.DialTimeOut = 5 * time.Second - } - config.Timeout = c.DialTimeOut - - config.HostKeyCallback = gossh.InsecureIgnoreHostKey() - proto := "tcp" - if strings.Contains(c.Addr, ":") { - proto = "tcp6" - } - client, err := gossh.Dial(proto, addr, config) - if nil != err { - return c, err - } - c.Client = client - return c, nil -} - -func (c *ConnInfo) Run(shell string) (string, error) { - if c.Client == nil { - if _, err := c.NewClient(); err != nil { - return "", err - } - } - session, err := c.Client.NewSession() - if err != nil { - return "", err - } - defer session.Close() - buf, err := session.CombinedOutput(shell) - - c.LastResult = string(buf) - return c.LastResult, err -} - -func (c *ConnInfo) Close() { - _ = c.Client.Close() -} - -type SshConn struct { - StdinPipe io.WriteCloser - ComboOutput *wsBufferWriter - Session *gossh.Session -} - -func (c *ConnInfo) NewSshConn(cols, rows int) (*SshConn, error) { - sshSession, err := c.Client.NewSession() - if err != nil { - return nil, err - } - - stdinP, err := sshSession.StdinPipe() - if err != nil { - return nil, err - } - - comboWriter := new(wsBufferWriter) - sshSession.Stdout = comboWriter - sshSession.Stderr = comboWriter - - modes := gossh.TerminalModes{ - gossh.ECHO: 1, - gossh.TTY_OP_ISPEED: 14400, - gossh.TTY_OP_OSPEED: 14400, - } - if err := sshSession.RequestPty("xterm", rows, cols, modes); err != nil { - return nil, err - } - if err := sshSession.Shell(); err != nil { - return nil, err - } - return &SshConn{StdinPipe: stdinP, ComboOutput: comboWriter, Session: sshSession}, nil -} - -func (s *SshConn) Close() { - if s.Session != nil { - s.Session.Close() - } -} - -type wsBufferWriter struct { - buffer bytes.Buffer - mu sync.Mutex -} - -func (w *wsBufferWriter) Write(p []byte) (int, error) { - w.mu.Lock() - defer w.mu.Unlock() - return w.buffer.Write(p) -} - -func makePrivateKeySigner(privateKey []byte, passPhrase []byte) (gossh.Signer, error) { - if len(passPhrase) != 0 { - return gossh.ParsePrivateKeyWithPassphrase(privateKey, passPhrase) - } - return gossh.ParsePrivateKey(privateKey) -} diff --git a/backend/utils/ssl/acme.go b/backend/utils/ssl/acme.go deleted file mode 100644 index 0d140dcf6..000000000 --- a/backend/utils/ssl/acme.go +++ /dev/null @@ -1,205 +0,0 @@ -package ssl - -import ( - "crypto" - "crypto/ecdsa" - "crypto/rsa" - "crypto/x509" - "encoding/json" - "encoding/pem" - "fmt" - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/go-acme/lego/v4/certcrypto" - "github.com/go-acme/lego/v4/lego" - "github.com/go-acme/lego/v4/registration" - "io" - "net/http" - "net/url" -) - -type domainError struct { - Domain string - Error error -} - -type zeroSSLRes struct { - Success bool `json:"success"` - EabKid string `json:"eab_kid"` - EabHmacKey string `json:"eab_hmac_key"` -} - -type KeyType = certcrypto.KeyType - -const ( - KeyEC256 = certcrypto.EC256 - KeyEC384 = certcrypto.EC384 - KeyRSA2048 = certcrypto.RSA2048 - KeyRSA3072 = certcrypto.RSA3072 - KeyRSA4096 = certcrypto.RSA4096 -) - -func GetPrivateKey(priKey crypto.PrivateKey, keyType KeyType) ([]byte, error) { - var ( - marshal []byte - block *pem.Block - err error - ) - - switch keyType { - case KeyEC256, KeyEC384: - key := priKey.(*ecdsa.PrivateKey) - marshal, err = x509.MarshalECPrivateKey(key) - if err != nil { - return nil, err - } - block = &pem.Block{ - Type: "EC PRIVATE KEY", - Bytes: marshal, - } - case KeyRSA2048, KeyRSA3072, KeyRSA4096: - key := priKey.(*rsa.PrivateKey) - marshal = x509.MarshalPKCS1PrivateKey(key) - block = &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: marshal, - } - } - - return pem.EncodeToMemory(block), nil -} - -func NewRegisterClient(acmeAccount *model.WebsiteAcmeAccount) (*AcmeClient, error) { - var ( - priKey crypto.PrivateKey - err error - ) - - if acmeAccount.PrivateKey != "" { - switch KeyType(acmeAccount.KeyType) { - case KeyEC256, KeyEC384: - block, _ := pem.Decode([]byte(acmeAccount.PrivateKey)) - priKey, err = x509.ParseECPrivateKey(block.Bytes) - if err != nil { - return nil, err - } - case KeyRSA2048, KeyRSA3072, KeyRSA4096: - block, _ := pem.Decode([]byte(acmeAccount.PrivateKey)) - priKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - return nil, err - } - } - - } else { - priKey, err = certcrypto.GeneratePrivateKey(KeyType(acmeAccount.KeyType)) - if err != nil { - return nil, err - } - } - - myUser := &AcmeUser{ - Email: acmeAccount.Email, - Key: priKey, - } - config := newConfig(myUser, acmeAccount.Type) - client, err := lego.NewClient(config) - if err != nil { - return nil, err - } - var reg *registration.Resource - if acmeAccount.Type == "zerossl" || acmeAccount.Type == "google" { - if acmeAccount.Type == "zerossl" { - var res *zeroSSLRes - res, err = getZeroSSLEabCredentials(acmeAccount.Email) - if err != nil { - return nil, err - } - if res.Success { - acmeAccount.EabKid = res.EabKid - acmeAccount.EabHmacKey = res.EabHmacKey - } else { - return nil, fmt.Errorf("get zero ssl eab credentials failed") - } - } - - eabOptions := registration.RegisterEABOptions{ - TermsOfServiceAgreed: true, - Kid: acmeAccount.EabKid, - HmacEncoded: acmeAccount.EabHmacKey, - } - reg, err = client.Registration.RegisterWithExternalAccountBinding(eabOptions) - if err != nil { - return nil, err - } - } else { - reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) - if err != nil { - return nil, err - } - } - myUser.Registration = reg - - acmeClient := &AcmeClient{ - User: myUser, - Client: client, - Config: config, - } - - return acmeClient, nil -} - -func newConfig(user *AcmeUser, accountType string) *lego.Config { - config := lego.NewConfig(user) - switch accountType { - case "letsencrypt": - config.CADirURL = "https://acme-v02.api.letsencrypt.org/directory" - case "zerossl": - config.CADirURL = "https://acme.zerossl.com/v2/DV90" - case "buypass": - config.CADirURL = "https://api.buypass.com/acme/directory" - case "google": - config.CADirURL = "https://dv.acme-v02.api.pki.goog/directory" - } - - config.UserAgent = "1Panel" - config.Certificate.KeyType = certcrypto.RSA2048 - return config -} - -func getZeroSSLEabCredentials(email string) (*zeroSSLRes, error) { - baseURL := "https://api.zerossl.com/acme/eab-credentials-email" - params := url.Values{} - params.Add("email", email) - requestURL := fmt.Sprintf("%s?%s", baseURL, params.Encode()) - - req, err := http.NewRequest("POST", requestURL, nil) - if err != nil { - return nil, err - } - - req.Header.Set("Content-Type", "application/json") - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("server returned non-200 status: %d %s", resp.StatusCode, http.StatusText(resp.StatusCode)) - } - - var result zeroSSLRes - err = json.Unmarshal(body, &result) - if err != nil { - return nil, err - } - - return &result, nil -} diff --git a/backend/utils/ssl/client.go b/backend/utils/ssl/client.go deleted file mode 100644 index e3cfd7b3b..000000000 --- a/backend/utils/ssl/client.go +++ /dev/null @@ -1,302 +0,0 @@ -package ssl - -import ( - "crypto" - "encoding/json" - "os" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/go-acme/lego/v4/acme" - "github.com/go-acme/lego/v4/acme/api" - "github.com/go-acme/lego/v4/certificate" - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/challenge/dns01" - "github.com/go-acme/lego/v4/lego" - "github.com/go-acme/lego/v4/providers/dns/alidns" - "github.com/go-acme/lego/v4/providers/dns/cloudflare" - "github.com/go-acme/lego/v4/providers/dns/dnspod" - "github.com/go-acme/lego/v4/providers/dns/godaddy" - "github.com/go-acme/lego/v4/providers/dns/namecheap" - "github.com/go-acme/lego/v4/providers/dns/namedotcom" - "github.com/go-acme/lego/v4/providers/dns/namesilo" - "github.com/go-acme/lego/v4/providers/dns/tencentcloud" - "github.com/go-acme/lego/v4/providers/http/webroot" - "github.com/go-acme/lego/v4/registration" - "github.com/pkg/errors" -) - -type AcmeUser struct { - Email string - Registration *registration.Resource - Key crypto.PrivateKey -} - -func (u *AcmeUser) GetEmail() string { - return u.Email -} - -func (u *AcmeUser) GetRegistration() *registration.Resource { - return u.Registration -} -func (u *AcmeUser) GetPrivateKey() crypto.PrivateKey { - return u.Key -} - -type AcmeClient struct { - Config *lego.Config - Client *lego.Client - User *AcmeUser -} - -func NewAcmeClient(acmeAccount *model.WebsiteAcmeAccount) (*AcmeClient, error) { - if acmeAccount.Email == "" { - return nil, errors.New("email can not blank") - } - client, err := NewRegisterClient(acmeAccount) - if err != nil { - return nil, err - } - return client, nil -} - -type DnsType string - -const ( - DnsPod DnsType = "DnsPod" - AliYun DnsType = "AliYun" - CloudFlare DnsType = "CloudFlare" - NameSilo DnsType = "NameSilo" - NameCheap DnsType = "NameCheap" - NameCom DnsType = "NameCom" - Godaddy DnsType = "Godaddy" - TencentCloud DnsType = "TencentCloud" -) - -type DNSParam struct { - ID string `json:"id"` - Token string `json:"token"` - AccessKey string `json:"accessKey"` - SecretKey string `json:"secretKey"` - Email string `json:"email"` - APIkey string `json:"apiKey"` - APIUser string `json:"apiUser"` - APISecret string `json:"apiSecret"` - SecretID string `json:"secretID"` -} - -var ( - propagationTimeout = 30 * time.Minute - pollingInterval = 10 * time.Second - ttl = 3600 -) - -func (c *AcmeClient) UseDns(dnsType DnsType, params string, websiteSSL model.WebsiteSSL) error { - var ( - param DNSParam - p challenge.Provider - err error - ) - - if err = json.Unmarshal([]byte(params), ¶m); err != nil { - return err - } - - switch dnsType { - case DnsPod: - dnsPodConfig := dnspod.NewDefaultConfig() - dnsPodConfig.LoginToken = param.ID + "," + param.Token - dnsPodConfig.PropagationTimeout = propagationTimeout - dnsPodConfig.PollingInterval = pollingInterval - dnsPodConfig.TTL = ttl - p, err = dnspod.NewDNSProviderConfig(dnsPodConfig) - case AliYun: - alidnsConfig := alidns.NewDefaultConfig() - alidnsConfig.SecretKey = param.SecretKey - alidnsConfig.APIKey = param.AccessKey - alidnsConfig.PropagationTimeout = propagationTimeout - alidnsConfig.PollingInterval = pollingInterval - alidnsConfig.TTL = ttl - p, err = alidns.NewDNSProviderConfig(alidnsConfig) - case CloudFlare: - cloudflareConfig := cloudflare.NewDefaultConfig() - cloudflareConfig.AuthEmail = param.Email - cloudflareConfig.AuthToken = param.APIkey - cloudflareConfig.PropagationTimeout = propagationTimeout - cloudflareConfig.PollingInterval = pollingInterval - cloudflareConfig.TTL = 3600 - p, err = cloudflare.NewDNSProviderConfig(cloudflareConfig) - case NameCheap: - namecheapConfig := namecheap.NewDefaultConfig() - namecheapConfig.APIKey = param.APIkey - namecheapConfig.APIUser = param.APIUser - namecheapConfig.PropagationTimeout = propagationTimeout - namecheapConfig.PollingInterval = pollingInterval - namecheapConfig.TTL = ttl - p, err = namecheap.NewDNSProviderConfig(namecheapConfig) - case NameSilo: - nameSiloConfig := namesilo.NewDefaultConfig() - nameSiloConfig.APIKey = param.APIkey - nameSiloConfig.PropagationTimeout = propagationTimeout - nameSiloConfig.PollingInterval = pollingInterval - nameSiloConfig.TTL = ttl - p, err = namesilo.NewDNSProviderConfig(nameSiloConfig) - case Godaddy: - godaddyConfig := godaddy.NewDefaultConfig() - godaddyConfig.APIKey = param.APIkey - godaddyConfig.APISecret = param.APISecret - godaddyConfig.PropagationTimeout = propagationTimeout - godaddyConfig.PollingInterval = pollingInterval - godaddyConfig.TTL = ttl - p, err = godaddy.NewDNSProviderConfig(godaddyConfig) - case NameCom: - nameComConfig := namedotcom.NewDefaultConfig() - nameComConfig.APIToken = param.Token - nameComConfig.Username = param.APIUser - nameComConfig.PropagationTimeout = propagationTimeout - nameComConfig.PollingInterval = pollingInterval - nameComConfig.TTL = ttl - p, err = namedotcom.NewDNSProviderConfig(nameComConfig) - case TencentCloud: - tencentCloudConfig := tencentcloud.NewDefaultConfig() - tencentCloudConfig.SecretID = param.SecretID - tencentCloudConfig.SecretKey = param.SecretKey - tencentCloudConfig.PropagationTimeout = propagationTimeout - tencentCloudConfig.PollingInterval = pollingInterval - tencentCloudConfig.TTL = ttl - p, err = tencentcloud.NewDNSProviderConfig(tencentCloudConfig) - } - if err != nil { - return err - } - var nameservers []string - if websiteSSL.Nameserver1 != "" { - nameservers = append(nameservers, websiteSSL.Nameserver1) - } - if websiteSSL.Nameserver2 != "" { - nameservers = append(nameservers, websiteSSL.Nameserver2) - } - if websiteSSL.DisableCNAME { - _ = os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", "true") - } else { - _ = os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", "false") - } - - return c.Client.Challenge.SetDNS01Provider(p, - dns01.CondOption(len(nameservers) > 0, - dns01.AddRecursiveNameservers(nameservers)), - dns01.CondOption(websiteSSL.SkipDNS, - dns01.DisableCompletePropagationRequirement()), - dns01.AddDNSTimeout(10*time.Minute), - ) -} - -func (c *AcmeClient) UseManualDns() error { - p := &manualDnsProvider{} - if err := c.Client.Challenge.SetDNS01Provider(p, dns01.AddDNSTimeout(10*time.Minute)); err != nil { - return err - } - return nil -} - -func (c *AcmeClient) UseHTTP(path string) error { - httpProvider, err := webroot.NewHTTPProvider(path) - if err != nil { - return err - } - - err = c.Client.Challenge.SetHTTP01Provider(httpProvider) - if err != nil { - return err - } - return nil -} - -func (c *AcmeClient) ObtainSSL(domains []string, privateKey crypto.PrivateKey) (certificate.Resource, error) { - request := certificate.ObtainRequest{ - Domains: domains, - Bundle: true, - PrivateKey: privateKey, - } - - certificates, err := c.Client.Certificate.Obtain(request) - if err != nil { - return certificate.Resource{}, err - } - - return *certificates, nil -} - -type Resolve struct { - Key string - Value string - Err string -} - -type manualDnsProvider struct { - Resolve *Resolve -} - -func (p *manualDnsProvider) Present(domain, token, keyAuth string) error { - return nil -} - -func (p *manualDnsProvider) CleanUp(domain, token, keyAuth string) error { - return nil -} - -func (c *AcmeClient) GetDNSResolve(domains []string) (map[string]Resolve, error) { - core, err := api.New(c.Config.HTTPClient, c.Config.UserAgent, c.Config.CADirURL, c.User.Registration.URI, c.User.Key) - if err != nil { - return nil, err - } - order, err := core.Orders.New(domains) - if err != nil { - return nil, err - } - resolves := make(map[string]Resolve) - resc, errc := make(chan acme.Authorization), make(chan domainError) - for _, authzURL := range order.Authorizations { - go func(authzURL string) { - authz, err := core.Authorizations.Get(authzURL) - if err != nil { - errc <- domainError{Domain: authz.Identifier.Value, Error: err} - return - } - resc <- authz - }(authzURL) - } - - var responses []acme.Authorization - for i := 0; i < len(order.Authorizations); i++ { - select { - case res := <-resc: - responses = append(responses, res) - case err := <-errc: - resolves[err.Domain] = Resolve{Err: err.Error.Error()} - } - } - close(resc) - close(errc) - - for _, auth := range responses { - domain := challenge.GetTargetedDomain(auth) - chlng, err := challenge.FindChallenge(challenge.DNS01, auth) - if err != nil { - resolves[domain] = Resolve{Err: err.Error()} - continue - } - keyAuth, err := core.GetKeyAuthorization(chlng.Token) - if err != nil { - resolves[domain] = Resolve{Err: err.Error()} - continue - } - challengeInfo := dns01.GetChallengeInfo(domain, keyAuth) - resolves[domain] = Resolve{ - Key: challengeInfo.FQDN, - Value: challengeInfo.Value, - } - } - - return resolves, nil -} diff --git a/backend/utils/systemctl/systemctl.go b/backend/utils/systemctl/systemctl.go deleted file mode 100644 index 0f7ef19db..000000000 --- a/backend/utils/systemctl/systemctl.go +++ /dev/null @@ -1,64 +0,0 @@ -package systemctl - -import ( - "fmt" - "github.com/pkg/errors" - "os/exec" - "strings" -) - -func RunSystemCtl(args ...string) (string, error) { - cmd := exec.Command("systemctl", args...) - output, err := cmd.CombinedOutput() - if err != nil { - return string(output), fmt.Errorf("failed to run command: %w", err) - } - return string(output), nil -} - -func IsActive(serviceName string) (bool, error) { - out, err := RunSystemCtl("is-active", serviceName) - if err != nil { - return false, err - } - return out == "active\n", nil -} - -func IsEnable(serviceName string) (bool, error) { - out, err := RunSystemCtl("is-enabled", serviceName) - if err != nil { - return false, err - } - return out == "enabled\n", nil -} - -func IsExist(serviceName string) (bool, error) { - out, err := RunSystemCtl("is-enabled", serviceName) - if err != nil { - if strings.Contains(out, "disabled") { - return true, nil - } - return false, nil - } - return true, nil -} - -func handlerErr(out string, err error) error { - if err != nil { - if out != "" { - return errors.New(out) - } - return err - } - return nil -} - -func Restart(serviceName string) error { - out, err := RunSystemCtl("restart", serviceName) - return handlerErr(out, err) -} - -func Operate(operate, serviceName string) error { - out, err := RunSystemCtl(operate, serviceName) - return handlerErr(out, err) -} diff --git a/backend/utils/terminal/local_cmd.go b/backend/utils/terminal/local_cmd.go deleted file mode 100644 index 098dbcea3..000000000 --- a/backend/utils/terminal/local_cmd.go +++ /dev/null @@ -1,93 +0,0 @@ -package terminal - -import ( - "os" - "os/exec" - "syscall" - "time" - "unsafe" - - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/creack/pty" - "github.com/pkg/errors" -) - -const ( - DefaultCloseSignal = syscall.SIGINT - DefaultCloseTimeout = 10 * time.Second -) - -type LocalCommand struct { - closeSignal syscall.Signal - closeTimeout time.Duration - - cmd *exec.Cmd - pty *os.File -} - -func NewCommand(commands []string) (*LocalCommand, error) { - cmd := exec.Command("docker", commands...) - - pty, err := pty.Start(cmd) - if err != nil { - return nil, errors.Wrapf(err, "failed to start command") - } - - lcmd := &LocalCommand{ - closeSignal: DefaultCloseSignal, - closeTimeout: DefaultCloseTimeout, - - cmd: cmd, - pty: pty, - } - - return lcmd, nil -} - -func (lcmd *LocalCommand) Read(p []byte) (n int, err error) { - return lcmd.pty.Read(p) -} - -func (lcmd *LocalCommand) Write(p []byte) (n int, err error) { - return lcmd.pty.Write(p) -} - -func (lcmd *LocalCommand) Close() error { - if lcmd.cmd != nil && lcmd.cmd.Process != nil { - _ = lcmd.cmd.Process.Kill() - } - _ = lcmd.pty.Close() - return nil -} - -func (lcmd *LocalCommand) ResizeTerminal(width int, height int) error { - window := struct { - row uint16 - col uint16 - x uint16 - y uint16 - }{ - uint16(height), - uint16(width), - 0, - 0, - } - _, _, errno := syscall.Syscall( - syscall.SYS_IOCTL, - lcmd.pty.Fd(), - syscall.TIOCSWINSZ, - uintptr(unsafe.Pointer(&window)), - ) - if errno != 0 { - return errno - } else { - return nil - } -} - -func (lcmd *LocalCommand) Wait(quitChan chan bool) { - if err := lcmd.cmd.Wait(); err != nil { - global.LOG.Errorf("ssh session wait failed, err: %v", err) - setQuit(quitChan) - } -} diff --git a/backend/utils/terminal/ws_local_session.go b/backend/utils/terminal/ws_local_session.go deleted file mode 100644 index 1a7a99adf..000000000 --- a/backend/utils/terminal/ws_local_session.go +++ /dev/null @@ -1,122 +0,0 @@ -package terminal - -import ( - "encoding/base64" - "encoding/json" - "sync" - - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/gorilla/websocket" - "github.com/pkg/errors" -) - -type LocalWsSession struct { - slave *LocalCommand - wsConn *websocket.Conn - - allowCtrlC bool - writeMutex sync.Mutex -} - -func NewLocalWsSession(cols, rows int, wsConn *websocket.Conn, slave *LocalCommand, allowCtrlC bool) (*LocalWsSession, error) { - if err := slave.ResizeTerminal(cols, rows); err != nil { - global.LOG.Errorf("ssh pty change windows size failed, err: %v", err) - } - - return &LocalWsSession{ - slave: slave, - wsConn: wsConn, - - allowCtrlC: allowCtrlC, - }, nil -} - -func (sws *LocalWsSession) Start(quitChan chan bool) { - go sws.handleSlaveEvent(quitChan) - go sws.receiveWsMsg(quitChan) -} - -func (sws *LocalWsSession) handleSlaveEvent(exitCh chan bool) { - defer setQuit(exitCh) - defer global.LOG.Debug("thread of handle slave event has exited now") - - buffer := make([]byte, 1024) - for { - select { - case <-exitCh: - return - default: - n, _ := sws.slave.Read(buffer) - _ = sws.masterWrite(buffer[:n]) - } - } -} - -func (sws *LocalWsSession) masterWrite(data []byte) error { - sws.writeMutex.Lock() - defer sws.writeMutex.Unlock() - wsData, err := json.Marshal(WsMsg{ - Type: WsMsgCmd, - Data: base64.StdEncoding.EncodeToString(data), - }) - if err != nil { - return errors.Wrapf(err, "failed to encoding to json") - } - err = sws.wsConn.WriteMessage(websocket.TextMessage, wsData) - if err != nil { - return errors.Wrapf(err, "failed to write to master") - } - return nil -} - -func (sws *LocalWsSession) receiveWsMsg(exitCh chan bool) { - defer func() { - if r := recover(); r != nil { - global.LOG.Errorf("A panic occurred during receive ws message, error message: %v", r) - } - }() - wsConn := sws.wsConn - defer setQuit(exitCh) - defer global.LOG.Debug("thread of receive ws msg has exited now") - for { - select { - case <-exitCh: - return - default: - _, wsData, err := wsConn.ReadMessage() - if err != nil { - global.LOG.Errorf("reading webSocket message failed, err: %v", err) - return - } - msgObj := WsMsg{} - _ = json.Unmarshal(wsData, &msgObj) - switch msgObj.Type { - case WsMsgResize: - if msgObj.Cols > 0 && msgObj.Rows > 0 { - if err := sws.slave.ResizeTerminal(msgObj.Cols, msgObj.Rows); err != nil { - global.LOG.Errorf("ssh pty change windows size failed, err: %v", err) - } - } - case WsMsgCmd: - decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Data) - if err != nil { - global.LOG.Errorf("websock cmd string base64 decoding failed, err: %v", err) - } - if string(decodeBytes) != "\x03" || sws.allowCtrlC { - sws.sendWebsocketInputCommandToSshSessionStdinPipe(decodeBytes) - } - case WsMsgHeartbeat: - err = wsConn.WriteMessage(websocket.TextMessage, wsData) - if err != nil { - global.LOG.Errorf("ssh sending heartbeat to webSocket failed, err: %v", err) - } - } - } - } -} - -func (sws *LocalWsSession) sendWebsocketInputCommandToSshSessionStdinPipe(cmdBytes []byte) { - if _, err := sws.slave.Write(cmdBytes); err != nil { - global.LOG.Errorf("ws cmd bytes write to ssh.stdin pipe failed, err: %v", err) - } -} diff --git a/backend/utils/terminal/ws_session.go b/backend/utils/terminal/ws_session.go deleted file mode 100644 index 2d66fab2f..000000000 --- a/backend/utils/terminal/ws_session.go +++ /dev/null @@ -1,218 +0,0 @@ -package terminal - -import ( - "bytes" - "encoding/base64" - "encoding/json" - "io" - "sync" - "time" - - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/gorilla/websocket" - "golang.org/x/crypto/ssh" -) - -type safeBuffer struct { - buffer bytes.Buffer - mu sync.Mutex -} - -func (w *safeBuffer) Write(p []byte) (int, error) { - w.mu.Lock() - defer w.mu.Unlock() - return w.buffer.Write(p) -} -func (w *safeBuffer) Bytes() []byte { - w.mu.Lock() - defer w.mu.Unlock() - return w.buffer.Bytes() -} -func (w *safeBuffer) Reset() { - w.mu.Lock() - defer w.mu.Unlock() - w.buffer.Reset() -} - -const ( - WsMsgCmd = "cmd" - WsMsgResize = "resize" - WsMsgHeartbeat = "heartbeat" -) - -type WsMsg struct { - Type string `json:"type"` - Data string `json:"data,omitempty"` // WsMsgCmd - Cols int `json:"cols,omitempty"` // WsMsgResize - Rows int `json:"rows,omitempty"` // WsMsgResize - Timestamp int `json:"timestamp,omitempty"` // WsMsgHeartbeat -} - -type LogicSshWsSession struct { - stdinPipe io.WriteCloser - comboOutput *safeBuffer - logBuff *safeBuffer - inputFilterBuff *safeBuffer - session *ssh.Session - wsConn *websocket.Conn - isAdmin bool - IsFlagged bool -} - -func NewLogicSshWsSession(cols, rows int, isAdmin bool, sshClient *ssh.Client, wsConn *websocket.Conn) (*LogicSshWsSession, error) { - sshSession, err := sshClient.NewSession() - if err != nil { - return nil, err - } - - stdinP, err := sshSession.StdinPipe() - if err != nil { - return nil, err - } - - comboWriter := new(safeBuffer) - logBuf := new(safeBuffer) - inputBuf := new(safeBuffer) - sshSession.Stdout = comboWriter - sshSession.Stderr = comboWriter - - modes := ssh.TerminalModes{ - ssh.ECHO: 1, - ssh.TTY_OP_ISPEED: 14400, - ssh.TTY_OP_OSPEED: 14400, - } - if err := sshSession.RequestPty("xterm", rows, cols, modes); err != nil { - return nil, err - } - if err := sshSession.Shell(); err != nil { - return nil, err - } - return &LogicSshWsSession{ - stdinPipe: stdinP, - comboOutput: comboWriter, - logBuff: logBuf, - inputFilterBuff: inputBuf, - session: sshSession, - wsConn: wsConn, - isAdmin: isAdmin, - IsFlagged: false, - }, nil -} - -func (sws *LogicSshWsSession) Close() { - if sws.session != nil { - sws.session.Close() - } - if sws.logBuff != nil { - sws.logBuff = nil - } - if sws.comboOutput != nil { - sws.comboOutput = nil - } -} -func (sws *LogicSshWsSession) Start(quitChan chan bool) { - go sws.receiveWsMsg(quitChan) - go sws.sendComboOutput(quitChan) -} - -func (sws *LogicSshWsSession) receiveWsMsg(exitCh chan bool) { - defer func() { - if r := recover(); r != nil { - global.LOG.Errorf("[A panic occurred during receive ws message, error message: %v", r) - } - }() - wsConn := sws.wsConn - defer setQuit(exitCh) - for { - select { - case <-exitCh: - return - default: - _, wsData, err := wsConn.ReadMessage() - if err != nil { - return - } - msgObj := WsMsg{} - _ = json.Unmarshal(wsData, &msgObj) - switch msgObj.Type { - case WsMsgResize: - if msgObj.Cols > 0 && msgObj.Rows > 0 { - if err := sws.session.WindowChange(msgObj.Rows, msgObj.Cols); err != nil { - global.LOG.Errorf("ssh pty change windows size failed, err: %v", err) - } - } - case WsMsgCmd: - decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Data) - if err != nil { - global.LOG.Errorf("websock cmd string base64 decoding failed, err: %v", err) - } - sws.sendWebsocketInputCommandToSshSessionStdinPipe(decodeBytes) - case WsMsgHeartbeat: - // 接收到心跳包后将心跳包原样返回,可以用于网络延迟检测等情况 - err = wsConn.WriteMessage(websocket.TextMessage, wsData) - if err != nil { - global.LOG.Errorf("ssh sending heartbeat to webSocket failed, err: %v", err) - } - } - } - } -} - -func (sws *LogicSshWsSession) sendWebsocketInputCommandToSshSessionStdinPipe(cmdBytes []byte) { - if _, err := sws.stdinPipe.Write(cmdBytes); err != nil { - global.LOG.Errorf("ws cmd bytes write to ssh.stdin pipe failed, err: %v", err) - } -} - -func (sws *LogicSshWsSession) sendComboOutput(exitCh chan bool) { - wsConn := sws.wsConn - defer setQuit(exitCh) - - tick := time.NewTicker(time.Millisecond * time.Duration(60)) - defer tick.Stop() - for { - select { - case <-tick.C: - if sws.comboOutput == nil { - return - } - bs := sws.comboOutput.Bytes() - if len(bs) > 0 { - wsData, err := json.Marshal(WsMsg{ - Type: WsMsgCmd, - Data: base64.StdEncoding.EncodeToString(bs), - }) - if err != nil { - global.LOG.Errorf("encoding combo output to json failed, err: %v", err) - continue - } - err = wsConn.WriteMessage(websocket.TextMessage, wsData) - if err != nil { - global.LOG.Errorf("ssh sending combo output to webSocket failed, err: %v", err) - } - _, err = sws.logBuff.Write(bs) - if err != nil { - global.LOG.Errorf("combo output to log buffer failed, err: %v", err) - } - sws.comboOutput.buffer.Reset() - } - if string(bs) == string([]byte{13, 10, 108, 111, 103, 111, 117, 116, 13, 10}) { - sws.Close() - return - } - - case <-exitCh: - return - } - } -} - -func (sws *LogicSshWsSession) Wait(quitChan chan bool) { - if err := sws.session.Wait(); err != nil { - setQuit(quitChan) - } -} - -func setQuit(ch chan bool) { - ch <- true -} diff --git a/backend/utils/toolbox/fail2ban.go b/backend/utils/toolbox/fail2ban.go deleted file mode 100644 index ce7f9c715..000000000 --- a/backend/utils/toolbox/fail2ban.go +++ /dev/null @@ -1,181 +0,0 @@ -package toolbox - -import ( - "fmt" - "os" - "strings" - - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/systemctl" -) - -type Fail2ban struct{} - -const defaultPath = "/etc/fail2ban/jail.local" - -type FirewallClient interface { - Status() (bool, bool, bool) - Version() (string, error) - Operate(operate string) error - OperateSSHD(operate, ip string) error -} - -func NewFail2Ban() (*Fail2ban, error) { - isExist, _ := systemctl.IsExist("fail2ban.service") - if isExist { - if _, err := os.Stat(defaultPath); err != nil { - if err := initLocalFile(); err != nil { - return nil, err - } - stdout, err := cmd.Exec("systemctl restart fail2ban.service") - if err != nil { - global.LOG.Errorf("restart fail2ban failed, err: %s", stdout) - return nil, err - } - } - } - return &Fail2ban{}, nil -} - -func (f *Fail2ban) Status() (bool, bool, bool) { - isEnable, _ := systemctl.IsEnable("fail2ban.service") - isActive, _ := systemctl.IsActive("fail2ban.service") - isExist, _ := systemctl.IsExist("fail2ban.service") - - return isEnable, isActive, isExist -} - -func (f *Fail2ban) Version() string { - stdout, err := cmd.Exec("fail2ban-client version") - if err != nil { - global.LOG.Errorf("load the fail2ban version failed, err: %s", stdout) - return "-" - } - return strings.ReplaceAll(stdout, "\n", "") -} - -func (f *Fail2ban) Operate(operate string) error { - switch operate { - case "start", "restart", "stop", "enable", "disable": - stdout, err := cmd.Execf("systemctl %s fail2ban.service", operate) - if err != nil { - return fmt.Errorf("%s the fail2ban.service failed, err: %s", operate, stdout) - } - return nil - case "reload": - stdout, err := cmd.Exec("fail2ban-client reload") - if err != nil { - return fmt.Errorf("fail2ban-client reload, err: %s", stdout) - } - return nil - default: - return fmt.Errorf("not support such operation: %v", operate) - } -} - -func (f *Fail2ban) ReBanIPs(ips []string) error { - ipItems, _ := f.ListBanned() - stdout, err := cmd.Execf("fail2ban-client unban --all") - if err != nil { - stdout1, err := cmd.Execf("fail2ban-client set sshd banip %s", strings.Join(ipItems, " ")) - if err != nil { - global.LOG.Errorf("rebanip after fail2ban-client unban --all failed, err: %s", stdout1) - } - return fmt.Errorf("fail2ban-client unban --all failed, err: %s", stdout) - } - stdout1, err := cmd.Execf("fail2ban-client set sshd banip %s", strings.Join(ips, " ")) - if err != nil { - return fmt.Errorf("handle `fail2ban-client set sshd banip %s` failed, err: %s", strings.Join(ips, " "), stdout1) - } - return nil -} - -func (f *Fail2ban) ListBanned() ([]string, error) { - var lists []string - stdout, err := cmd.Exec("fail2ban-client status sshd | grep 'Banned IP list:'") - if err != nil { - return lists, err - } - itemList := strings.Split(strings.Trim(stdout, "\n"), "Banned IP list:") - if len(itemList) != 2 { - return lists, nil - } - - ips := strings.Fields(itemList[1]) - for _, item := range ips { - if len(item) != 0 { - lists = append(lists, item) - } - } - return lists, nil -} - -func (f *Fail2ban) ListIgnore() ([]string, error) { - var lists []string - stdout, err := cmd.Exec("fail2ban-client get sshd ignoreip") - if err != nil { - return lists, err - } - stdout = strings.ReplaceAll(stdout, "|", "") - stdout = strings.ReplaceAll(stdout, "`", "") - stdout = strings.ReplaceAll(stdout, "\n", "") - addrs := strings.Split(stdout, "-") - for _, addr := range addrs { - if !strings.HasPrefix(addr, " ") { - continue - } - lists = append(lists, strings.ReplaceAll(addr, " ", "")) - } - return lists, nil -} - -func initLocalFile() error { - f, err := os.Create(defaultPath) - if err != nil { - return err - } - defer f.Close() - initFile := `#DEFAULT-START -[DEFAULT] -bantime = 600 -findtime = 300 -maxretry = 5 -banaction = $banaction -action = %(action_mwl)s -#DEFAULT-END - -[sshd] -ignoreip = 127.0.0.1/8 -enabled = true -filter = sshd -port = 22 -maxretry = 5 -findtime = 300 -bantime = 600 -banaction = $banaction -action = %(action_mwl)s -logpath = $logpath` - - banaction := "" - if active, _ := systemctl.IsActive("firewalld"); active { - banaction = "firewallcmd-ipset" - } else if active, _ := systemctl.IsActive("ufw"); active { - banaction = "ufw" - } else { - banaction = "iptables-allports" - } - initFile = strings.ReplaceAll(initFile, "$banaction", banaction) - - logPath := "" - if _, err := os.Stat("/var/log/secure"); err == nil { - logPath = "/var/log/secure" - } else { - logPath = "/var/log/auth.log" - } - initFile = strings.ReplaceAll(initFile, "$logpath", logPath) - if err := os.WriteFile(defaultPath, []byte(initFile), 0640); err != nil { - return err - } - return nil -} diff --git a/backend/utils/toolbox/pure-ftpd.go b/backend/utils/toolbox/pure-ftpd.go deleted file mode 100644 index 8538ea599..000000000 --- a/backend/utils/toolbox/pure-ftpd.go +++ /dev/null @@ -1,272 +0,0 @@ -package toolbox - -import ( - "errors" - "fmt" - "os" - "os/user" - "path" - "path/filepath" - "strings" - "time" - - "github.com/1Panel-dev/1Panel/backend/constant" - "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" - "github.com/1Panel-dev/1Panel/backend/utils/systemctl" -) - -type Ftp struct { - DefaultUser string - DefaultGroup string -} - -type FtpClient interface { - Status() (bool, bool) - Operate(operate string) error - LoadList() ([]FtpList, error) - UserAdd(username, path, passwd string) error - UserDel(username string) error - SetPasswd(username, passwd string) error - Reload() error - LoadLogs() ([]FtpLog, error) -} - -func NewFtpClient() (*Ftp, error) { - userItem, err := user.LookupId("1000") - if err == nil { - groupItem, err := user.LookupGroupId(userItem.Gid) - if err != nil { - return nil, err - } - return &Ftp{DefaultUser: userItem.Username, DefaultGroup: groupItem.Name}, err - } - if err.Error() != user.UnknownUserIdError(1000).Error() { - return nil, err - } - - groupItem, err := user.LookupGroupId("1000") - if err == nil { - stdout2, err := cmd.Execf("useradd -u 1000 -g %s %s", groupItem.Name, "1panel") - if err != nil { - return nil, errors.New(stdout2) - } - return &Ftp{DefaultUser: "1panel", DefaultGroup: groupItem.Name}, nil - } - if err.Error() != user.UnknownGroupIdError("1000").Error() { - return nil, err - } - stdout, err := cmd.Exec("groupadd -g 1000 1panel") - if err != nil { - return nil, errors.New(string(stdout)) - } - stdout2, err := cmd.Exec("useradd -u 1000 -g 1panel 1panel") - if err != nil { - return nil, errors.New(stdout2) - } - return &Ftp{DefaultUser: "1panel", DefaultGroup: "1panel"}, nil -} - -func (f *Ftp) Status() (bool, bool) { - isActive, _ := systemctl.IsActive("pure-ftpd.service") - isExist, _ := systemctl.IsExist("pure-ftpd.service") - - return isActive, isExist -} - -func (f *Ftp) Operate(operate string) error { - switch operate { - case "start", "restart", "stop": - stdout, err := cmd.Execf("systemctl %s pure-ftpd.service", operate) - if err != nil { - return fmt.Errorf("%s the pure-ftpd.service failed, err: %s", operate, stdout) - } - return nil - default: - return fmt.Errorf("not support such operation: %v", operate) - } -} - -func (f *Ftp) UserAdd(username, passwd, path string) error { - std, err := cmd.Execf("pure-pw useradd %s -u %s -d %s < 0 && processConfig.Pid != proc.Pid { - return - } - if procName, err := proc.Name(); err == nil { - procData.Name = procName - } else { - procData.Name = "" - } - if processConfig.Name != "" && !strings.Contains(procData.Name, processConfig.Name) { - return - } - if username, err := proc.Username(); err == nil { - procData.Username = username - } - if processConfig.Username != "" && !strings.Contains(procData.Username, processConfig.Username) { - return - } - procData.PPID, _ = proc.Ppid() - statusArray, _ := proc.Status() - if len(statusArray) > 0 { - procData.Status = strings.Join(statusArray, ",") - } - createTime, procErr := proc.CreateTime() - if procErr == nil { - t := time.Unix(createTime/1000, 0) - procData.StartTime = t.Format("2006-1-2 15:04:05") - } - procData.NumThreads, _ = proc.NumThreads() - connections, procErr := proc.Connections() - if procErr == nil { - procData.NumConnections = len(connections) - for _, conn := range connections { - if conn.Laddr.IP != "" || conn.Raddr.IP != "" { - procData.Connects = append(procData.Connects, processConnect{ - Status: conn.Status, - Laddr: conn.Laddr, - Raddr: conn.Raddr, - }) - } - } - } - procData.CpuValue, _ = proc.CPUPercent() - procData.CpuPercent = fmt.Sprintf("%.2f", procData.CpuValue) + "%" - menInfo, procErr := proc.MemoryInfo() - if procErr == nil { - procData.Rss = formatBytes(menInfo.RSS) - procData.RssValue = menInfo.RSS - procData.Data = formatBytes(menInfo.Data) - procData.VMS = formatBytes(menInfo.VMS) - procData.HWM = formatBytes(menInfo.HWM) - procData.Stack = formatBytes(menInfo.Stack) - procData.Locked = formatBytes(menInfo.Locked) - procData.Swap = formatBytes(menInfo.Swap) - } else { - procData.Rss = "--" - procData.Data = "--" - procData.VMS = "--" - procData.HWM = "--" - procData.Stack = "--" - procData.Locked = "--" - procData.Swap = "--" - - procData.RssValue = 0 - } - ioStat, procErr := proc.IOCounters() - if procErr == nil { - procData.DiskWrite = formatBytes(ioStat.WriteBytes) - procData.DiskRead = formatBytes(ioStat.ReadBytes) - } else { - procData.DiskWrite = "--" - procData.DiskRead = "--" - } - procData.CmdLine, _ = proc.Cmdline() - procData.OpenFiles, _ = proc.OpenFiles() - procData.Envs, _ = proc.Environ() - - resultMutex.Lock() - result = append(result, procData) - resultMutex.Unlock() - } - - chunkSize := (len(processes) + numWorkers - 1) / numWorkers - for i := 0; i < numWorkers; i++ { - wg.Add(1) - start := i * chunkSize - end := (i + 1) * chunkSize - if end > len(processes) { - end = len(processes) - } - - go func(start, end int) { - defer wg.Done() - for j := start; j < end; j++ { - handleData(processes[j]) - } - }(start, end) - } - - wg.Wait() - - sort.Slice(result, func(i, j int) bool { - return result[i].PID < result[j].PID - }) - res, err = json.Marshal(result) - return -} - -func getSSHSessions(config SSHSessionConfig) (res []byte, err error) { - var ( - result []sshSession - users []host.UserStat - processes []*process.Process - ) - processes, err = process.Processes() - if err != nil { - return - } - users, err = host.Users() - if err != nil { - return - } - for _, proc := range processes { - name, _ := proc.Name() - if name != "sshd" || proc.Pid == 0 { - continue - } - connections, _ := proc.Connections() - for _, conn := range connections { - for _, user := range users { - if user.Host == "" { - continue - } - if conn.Raddr.IP == user.Host { - if config.LoginUser != "" && !strings.Contains(user.User, config.LoginUser) { - continue - } - if config.LoginIP != "" && !strings.Contains(user.Host, config.LoginIP) { - continue - } - if terminal, err := proc.Cmdline(); err == nil { - if strings.Contains(terminal, user.Terminal) { - session := sshSession{ - Username: user.User, - Host: user.Host, - Terminal: user.Terminal, - PID: proc.Pid, - } - t := time.Unix(int64(user.Started), 0) - session.LoginTime = t.Format("2006-1-2 15:04:05") - result = append(result, session) - } - } - } - } - } - } - res, err = json.Marshal(result) - return -} - -var netTypes = [...]string{"tcp", "udp"} - -func getNetConnections(config NetConfig) (res []byte, err error) { - var ( - result []processConnect - proc *process.Process - ) - for _, netType := range netTypes { - connections, _ := net.Connections(netType) - if err == nil { - for _, conn := range connections { - if config.ProcessID > 0 && config.ProcessID != conn.Pid { - continue - } - proc, err = process.NewProcess(conn.Pid) - if err == nil { - name, _ := proc.Name() - if name != "" && config.ProcessName != "" && !strings.Contains(name, config.ProcessName) { - continue - } - if config.Port > 0 && config.Port != conn.Laddr.Port && config.Port != conn.Raddr.Port { - continue - } - result = append(result, processConnect{ - Type: netType, - Status: conn.Status, - Laddr: conn.Laddr, - Raddr: conn.Raddr, - PID: conn.Pid, - Name: name, - }) - } - - } - } - } - res, err = json.Marshal(result) - return -} diff --git a/backend/utils/xpack/xpack.go b/backend/utils/xpack/xpack.go deleted file mode 100644 index 14cf0b913..000000000 --- a/backend/utils/xpack/xpack.go +++ /dev/null @@ -1,37 +0,0 @@ -//go:build !xpack - -package xpack - -import ( - "crypto/tls" - "net" - "net/http" - "time" - - "github.com/1Panel-dev/1Panel/backend/app/model" - "github.com/1Panel-dev/1Panel/backend/buserr" - "github.com/1Panel-dev/1Panel/backend/constant" -) - -func RemoveTamper(website string) {} - -func LoadRequestTransport() *http.Transport { - return &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - DialContext: (&net.Dialer{ - Timeout: 60 * time.Second, - KeepAlive: 60 * time.Second, - }).DialContext, - TLSHandshakeTimeout: 5 * time.Second, - ResponseHeaderTimeout: 10 * time.Second, - IdleConnTimeout: 15 * time.Second, - } -} - -func LoadGpuInfo() []interface{} { - return nil -} - -func StartClam(startClam model.Clam, isUpdate bool) (int, error) { - return 0, buserr.New(constant.ErrXpackNotFound) -} diff --git a/cmd/server/app/app_config.go b/cmd/server/app/app_config.go deleted file mode 100644 index d4d155998..000000000 --- a/cmd/server/app/app_config.go +++ /dev/null @@ -1,14 +0,0 @@ -package app - -import ( - _ "embed" -) - -//go:embed app_config.yml -var Config []byte - -//go:embed logo.png -var Logo []byte - -//go:embed app_param.yml -var Param []byte diff --git a/cmd/server/app/app_config.yml b/cmd/server/app/app_config.yml deleted file mode 100644 index 3a2ec0827..000000000 --- a/cmd/server/app/app_config.yml +++ /dev/null @@ -1,13 +0,0 @@ -additionalProperties: - key: #应用的 key ,仅限英文,用于在 Linux 创建文件夹 - name: #应用名称 - tags: - - Tool #应用标签,可以有多个,请参照下方的标签列表 - shortDescZh: #应用中文描述,不要超过30个字 - shortDescEn: #应用英文描述 - type: tool #应用类型,区别于应用分类,只能有一个,请参照下方的类型列表 - crossVersionUpdate: #是否可以跨大版本升级 - limit: #应用安装数量限制,0 代表无限制 - website: #官网地址 - github: #github 地址 - document: #文档地址 \ No newline at end of file diff --git a/cmd/server/app/app_param.yml b/cmd/server/app/app_param.yml deleted file mode 100644 index 426fa78c6..000000000 --- a/cmd/server/app/app_param.yml +++ /dev/null @@ -1,10 +0,0 @@ -additionalProperties: - formFields: - - default: 8080 - edit: true - envKey: PANEL_APP_PORT_HTTP - labelEn: Port - labelZh: 端口 - required: true - rule: paramPort - type: number diff --git a/cmd/server/app/logo.png b/cmd/server/app/logo.png deleted file mode 100644 index 6f82a12d56b666914ee686321551411e970d4db5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9330 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 -404 Not Found - -

404 Not Found

-
nginx
- \ No newline at end of file diff --git a/cmd/server/nginx_conf/cache.conf b/cmd/server/nginx_conf/cache.conf deleted file mode 100644 index ef1285ba8..000000000 --- a/cmd/server/nginx_conf/cache.conf +++ /dev/null @@ -1,12 +0,0 @@ -proxy_temp_path /www/common/proxy/proxy_temp_dir; -proxy_cache_path /www/common/proxy/proxy_cache_dir levels=1:2 keys_zone=proxy_cache_panel:20m inactive=1d max_size=5g; -client_body_buffer_size 512k; -proxy_connect_timeout 60; -proxy_read_timeout 60; -proxy_send_timeout 60; -proxy_buffer_size 32k; -proxy_buffers 4 64k; -proxy_busy_buffers_size 128k; -proxy_temp_file_write_size 128k; -proxy_next_upstream error timeout invalid_header http_500 http_503 http_404; -proxy_cache proxy_cache_panel; \ No newline at end of file diff --git a/cmd/server/nginx_conf/domain404.html b/cmd/server/nginx_conf/domain404.html deleted file mode 100644 index d75ed7812..000000000 --- a/cmd/server/nginx_conf/domain404.html +++ /dev/null @@ -1,6 +0,0 @@ - -404 Not Found - -

404 Not Found

-
nginx
- \ No newline at end of file diff --git a/cmd/server/nginx_conf/gzip.conf b/cmd/server/nginx_conf/gzip.conf deleted file mode 100644 index b277f1f89..000000000 --- a/cmd/server/nginx_conf/gzip.conf +++ /dev/null @@ -1,4 +0,0 @@ -gzip on; -gzip_comp_level 6; -gzip_min_length 1k; -gzip_types text/plain text/css text/xml text/javascript text/x-component application/json application/javascript application/x-javascript application/xml application/xhtml+xml application/rss+xml application/atom+xml application/x-font-ttf application/vnd.ms-fontobject image/svg+xml image/x-icon font/opentype; \ No newline at end of file diff --git a/cmd/server/nginx_conf/index.html b/cmd/server/nginx_conf/index.html deleted file mode 100644 index 6c9cb573d..000000000 --- a/cmd/server/nginx_conf/index.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - 恭喜,站点创建成功! - - - -
-

恭喜, 站点创建成功!

-

这是默认index.html,本页面由系统自动生成

-
- - \ No newline at end of file diff --git a/cmd/server/nginx_conf/index.php b/cmd/server/nginx_conf/index.php deleted file mode 100644 index 5cd4e8c46..000000000 --- a/cmd/server/nginx_conf/index.php +++ /dev/null @@ -1,25 +0,0 @@ -欢迎使用 PHP!'; -echo '

版本信息

'; - -echo '
    '; -echo '
  • PHP版本:', PHP_VERSION, '
  • '; -echo '
'; - -echo '

已安装扩展

'; -printExtensions(); - -/** - * 获取已安装扩展列表 - */ -function printExtensions() -{ - echo '
    '; - foreach (get_loaded_extensions() as $i => $name) { - echo "
  1. ", $name, '=', phpversion($name), '
  2. '; - } - echo '
'; -} \ No newline at end of file diff --git a/cmd/server/nginx_conf/nginx_conf.go b/cmd/server/nginx_conf/nginx_conf.go deleted file mode 100644 index 7b376a9b0..000000000 --- a/cmd/server/nginx_conf/nginx_conf.go +++ /dev/null @@ -1,39 +0,0 @@ -package nginx_conf - -import ( - "embed" - _ "embed" -) - -//go:embed ssl.conf -var SSL []byte - -//go:embed website_default.conf -var WebsiteDefault []byte - -//go:embed index.html -var Index []byte - -//go:embed index.php -var IndexPHP []byte - -//go:embed rewrite/* -var Rewrites embed.FS - -//go:embed cache.conf -var Cache []byte - -//go:embed proxy.conf -var Proxy []byte - -//go:embed proxy_cache.conf -var ProxyCache []byte - -//go:embed 404.html -var NotFoundHTML []byte - -//go:embed domain404.html -var DomainNotFoundHTML []byte - -//go:embed stop.html -var StopHTML []byte diff --git a/cmd/server/nginx_conf/proxy.conf b/cmd/server/nginx_conf/proxy.conf deleted file mode 100644 index d1914fb69..000000000 --- a/cmd/server/nginx_conf/proxy.conf +++ /dev/null @@ -1,13 +0,0 @@ -location ^~ /test { - proxy_pass http://1panel.cloud/; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header REMOTE-HOST $remote_addr; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_http_version 1.1; - - add_header X-Cache $upstream_cache_status; -} diff --git a/cmd/server/nginx_conf/proxy_cache.conf b/cmd/server/nginx_conf/proxy_cache.conf deleted file mode 100644 index 45172d6b6..000000000 --- a/cmd/server/nginx_conf/proxy_cache.conf +++ /dev/null @@ -1,4 +0,0 @@ -proxy_ignore_headers Set-Cookie Cache-Control expires; -proxy_cache cache_one; -proxy_cache_key $host$uri$is_args$args; -proxy_cache_valid 200 304 301 302 10m; \ No newline at end of file diff --git a/cmd/server/nginx_conf/proxy_no_cache.conf b/cmd/server/nginx_conf/proxy_no_cache.conf deleted file mode 100644 index 597f25ac4..000000000 --- a/cmd/server/nginx_conf/proxy_no_cache.conf +++ /dev/null @@ -1,10 +0,0 @@ -set $static_fileg 0; -if ( $uri ~* "\.(gif|png|jpg|css|js|woff|woff2)$" ) -{ - set $static_fileg 1; - expires 1m; -} -if ( $static_fileg = 0 ) -{ - add_header Cache-Control no-cache; -} diff --git a/cmd/server/nginx_conf/rewrite/crmeb.conf b/cmd/server/nginx_conf/rewrite/crmeb.conf deleted file mode 100644 index 4260774ee..000000000 --- a/cmd/server/nginx_conf/rewrite/crmeb.conf +++ /dev/null @@ -1,6 +0,0 @@ -location / { - if (!-e $request_filename) { - rewrite ^(.*)$ /index.php?s=/$1 last; - break; - } -} diff --git a/cmd/server/nginx_conf/rewrite/dabr.conf b/cmd/server/nginx_conf/rewrite/dabr.conf deleted file mode 100644 index 37c13132f..000000000 --- a/cmd/server/nginx_conf/rewrite/dabr.conf +++ /dev/null @@ -1,5 +0,0 @@ -location / { -if (!-e $request_filename) { -rewrite ^/(.*)$ /index.php?q=$1 last; -} -} \ No newline at end of file diff --git a/cmd/server/nginx_conf/rewrite/dbshop.conf b/cmd/server/nginx_conf/rewrite/dbshop.conf deleted file mode 100644 index 61d23f711..000000000 --- a/cmd/server/nginx_conf/rewrite/dbshop.conf +++ /dev/null @@ -1,7 +0,0 @@ -location /{ - try_files $uri $uri/ /index.php$is_args$args; -} - -location ~ \.htaccess{ - deny all; -} \ No newline at end of file diff --git a/cmd/server/nginx_conf/rewrite/dedecms.conf b/cmd/server/nginx_conf/rewrite/dedecms.conf deleted file mode 100644 index 6e110cba5..000000000 --- a/cmd/server/nginx_conf/rewrite/dedecms.conf +++ /dev/null @@ -1,10 +0,0 @@ -rewrite "^/list-([0-9]+)\.html$" /plus/list.php?tid=$1 last; -rewrite "^/list-([0-9]+)-([0-9]+)-([0-9]+)\.html$" /plus/list.php?tid=$1&totalresult=$2&PageNo=$3 last; -rewrite "^/view-([0-9]+)-1\.html$" /plus/view.php?arcID=$1 last; -rewrite "^/view-([0-9]+)-([0-9]+)\.html$" /plus/view.php?aid=$1&pageno=$2 last; -rewrite "^/plus/list-([0-9]+)\.html$" /plus/list.php?tid=$1 last; -rewrite "^/plus/list-([0-9]+)-([0-9]+)-([0-9]+)\.html$" /plus/list.php?tid=$1&totalresult=$2&PageNo=$3 last; -rewrite "^/plus/view-([0-9]+)-1\.html$" /plus/view.php?arcID=$1 last; -rewrite "^/plus/view-([0-9]+)-([0-9]+)\.html$" /plus/view.php?aid=$1&pageno=$2 last; -rewrite "^/tags.html$" /tags.php last; -rewrite "^/tag-([0-9]+)-([0-9]+)\.html$" /tags.php?/$1/$2/ last; diff --git a/cmd/server/nginx_conf/rewrite/default.conf b/cmd/server/nginx_conf/rewrite/default.conf deleted file mode 100644 index e69de29bb..000000000 diff --git a/cmd/server/nginx_conf/rewrite/discuz.conf b/cmd/server/nginx_conf/rewrite/discuz.conf deleted file mode 100644 index 578da7653..000000000 --- a/cmd/server/nginx_conf/rewrite/discuz.conf +++ /dev/null @@ -1,7 +0,0 @@ -location / { - rewrite ^/archiver/((fid|tid)-[\w\-]+\.html)$ /archiver/index.php?$1 last; - rewrite ^/forum-([0-9]+)-([0-9]+)\.html$ /forumdisplay.php?fid=$1&page=$2 last; - rewrite ^/thread-([0-9]+)-([0-9]+)-([0-9]+)\.html$ /viewthread.php?tid=$1&extra=page%3D$3&page=$2 last; - rewrite ^/space-(username|uid)-(.+)\.html$ /space.php?$1=$2 last; - rewrite ^/tag-(.+)\.html$ /tag.php?name=$1 last; - } \ No newline at end of file diff --git a/cmd/server/nginx_conf/rewrite/discuzx.conf b/cmd/server/nginx_conf/rewrite/discuzx.conf deleted file mode 100644 index 8058495a2..000000000 --- a/cmd/server/nginx_conf/rewrite/discuzx.conf +++ /dev/null @@ -1,12 +0,0 @@ -rewrite ^([^\.]*)/topic-(.+)\.html$ $1/portal.php?mod=topic&topic=$2 last; -rewrite ^([^\.]*)/article-([0-9]+)-([0-9]+)\.html$ $1/portal.php?mod=view&aid=$2&page=$3 last; -rewrite ^([^\.]*)/forum-(\w+)-([0-9]+)\.html$ $1/forum.php?mod=forumdisplay&fid=$2&page=$3 last; -rewrite ^([^\.]*)/thread-([0-9]+)-([0-9]+)-([0-9]+)\.html$ $1/forum.php?mod=viewthread&tid=$2&extra=page%3D$4&page=$3 last; -rewrite ^([^\.]*)/group-([0-9]+)-([0-9]+)\.html$ $1/forum.php?mod=group&fid=$2&page=$3 last; -rewrite ^([^\.]*)/space-(username|uid)-(.+)\.html$ $1/home.php?mod=space&$2=$3 last; -rewrite ^([^\.]*)/blog-([0-9]+)-([0-9]+)\.html$ $1/home.php?mod=space&uid=$2&do=blog&id=$3 last; -rewrite ^([^\.]*)/(fid|tid)-([0-9]+)\.html$ $1/index.php?action=$2&value=$3 last; -rewrite ^([^\.]*)/([a-z]+[a-z0-9_]*)-([a-z0-9_\-]+)\.html$ $1/plugin.php?id=$2:$3 last; -if (!-e $request_filename) { - return 404; -} \ No newline at end of file diff --git a/cmd/server/nginx_conf/rewrite/discuzx2.conf b/cmd/server/nginx_conf/rewrite/discuzx2.conf deleted file mode 100644 index 61059e21e..000000000 --- a/cmd/server/nginx_conf/rewrite/discuzx2.conf +++ /dev/null @@ -1,14 +0,0 @@ -location /bbs/ { - rewrite ^([^\.]*)/topic-(.+)\.html$ $1/portal.php?mod=topic&topic=$2 last; - rewrite ^([^\.]*)/article-([0-9]+)-([0-9]+)\.html$ $1/portal.php?mod=view&aid=$2&page=$3 last; - rewrite ^([^\.]*)/forum-(\w+)-([0-9]+)\.html$ $1/forum.php?mod=forumdisplay&fid=$2&page=$3 last; - rewrite ^([^\.]*)/thread-([0-9]+)-([0-9]+)-([0-9]+)\.html$ $1/forum.php?mod=viewthread&tid=$2&extra=page%3D$4&page=$3 last; - rewrite ^([^\.]*)/group-([0-9]+)-([0-9]+)\.html$ $1/forum.php?mod=group&fid=$2&page=$3 last; - rewrite ^([^\.]*)/space-(username|uid)-(.+)\.html$ $1/home.php?mod=space&$2=$3 last; - rewrite ^([^\.]*)/blog-([0-9]+)-([0-9]+)\.html$ $1/home.php?mod=space&uid=$2&do=blog&id=$3 last; - rewrite ^([^\.]*)/(fid|tid)-([0-9]+)\.html$ $1/index.php?action=$2&value=$3 last; - rewrite ^([^\.]*)/([a-z]+[a-z0-9_]*)-([a-z0-9_\-]+)\.html$ $1/plugin.php?id=$2:$3 last; - if (!-e $request_filename) { - return 404; - } -} \ No newline at end of file diff --git a/cmd/server/nginx_conf/rewrite/discuzx3.conf b/cmd/server/nginx_conf/rewrite/discuzx3.conf deleted file mode 100644 index 661889734..000000000 --- a/cmd/server/nginx_conf/rewrite/discuzx3.conf +++ /dev/null @@ -1,15 +0,0 @@ -location / { - rewrite ^([^\.]*)/topic-(.+)\.html$ $1/portal.php?mod=topic&topic=$2 last; - rewrite ^([^\.]*)/article-([0-9]+)-([0-9]+)\.html$ $1/portal.php?mod=view&aid=$2&page=$3 last; - rewrite ^([^\.]*)/forum-(\w+)-([0-9]+)\.html$ $1/forum.php?mod=forumdisplay&fid=$2&page=$3 last; - rewrite ^([^\.]*)/thread-([0-9]+)-([0-9]+)-([0-9]+)\.html$ $1/forum.php?mod=viewthread&tid=$2&extra=page%3D$4&page=$3 last; - rewrite ^([^\.]*)/group-([0-9]+)-([0-9]+)\.html$ $1/forum.php?mod=group&fid=$2&page=$3 last; - rewrite ^([^\.]*)/space-(username|uid)-(.+)\.html$ $1/home.php?mod=space&$2=$3 last; - rewrite ^([^\.]*)/blog-([0-9]+)-([0-9]+)\.html$ $1/home.php?mod=space&uid=$2&do=blog&id=$3 last; - rewrite ^([^\.]*)/(fid|tid)-([0-9]+)\.html$ $1/index.php?action=$2&value=$3 last; - rewrite ^([^\.]*)/([a-z]+[a-z0-9_]*)-([a-z0-9_\-]+)\.html$ $1/plugin.php?id=$2:$3 last; - if (!-e $request_filename) { - return 404; - } -} - diff --git a/cmd/server/nginx_conf/rewrite/drupal.conf b/cmd/server/nginx_conf/rewrite/drupal.conf deleted file mode 100644 index 460b7791d..000000000 --- a/cmd/server/nginx_conf/rewrite/drupal.conf +++ /dev/null @@ -1,3 +0,0 @@ -if (!-e $request_filename) { - rewrite ^/(.*)$ /index.php?q=$1 last; - } \ No newline at end of file diff --git a/cmd/server/nginx_conf/rewrite/ecshop.conf b/cmd/server/nginx_conf/rewrite/ecshop.conf deleted file mode 100644 index 3574daa73..000000000 --- a/cmd/server/nginx_conf/rewrite/ecshop.conf +++ /dev/null @@ -1,32 +0,0 @@ -if (!-e $request_filename) -{ -rewrite "^/index\.html" /index.php last; -rewrite "^/category$" /index.php last; -rewrite "^/feed-c([0-9]+)\.xml$" /feed.php?cat=$1 last; -rewrite "^/feed-b([0-9]+)\.xml$" /feed.php?brand=$1 last; -rewrite "^/feed\.xml$" /feed.php last; -rewrite "^/category-([0-9]+)-b([0-9]+)-min([0-9]+)-max([0-9]+)-attr([^-]*)-([0-9]+)-(.+)-([a-zA-Z]+)(.*)\.html$" /category.php?id=$1&brand=$2&price_min=$3&price_max=$4&filter_attr=$5&page=$6&sort=$7&order=$8 last; -rewrite "^/category-([0-9]+)-b([0-9]+)-min([0-9]+)-max([0-9]+)-attr([^-]*)(.*)\.html$" /category.php?id=$1&brand=$2&price_min=$3&price_max=$4&filter_attr=$5 last; -rewrite "^/category-([0-9]+)-b([0-9]+)-([0-9]+)-(.+)-([a-zA-Z]+)(.*)\.html$" /category.php?id=$1&brand=$2&page=$3&sort=$4&order=$5 last; -rewrite "^/category-([0-9]+)-b([0-9]+)-([0-9]+)(.*)\.html$" /category.php?id=$1&brand=$2&page=$3 last; -rewrite "^/category-([0-9]+)-b([0-9]+)(.*)\.html$" /category.php?id=$1&brand=$2 last; -rewrite "^/category-([0-9]+)(.*)\.html$" /category.php?id=$1 last; -rewrite "^/goods-([0-9]+)(.*)\.html" /goods.php?id=$1 last; -rewrite "^/article_cat-([0-9]+)-([0-9]+)-(.+)-([a-zA-Z]+)(.*)\.html$" /article_cat.php?id=$1&page=$2&sort=$3&order=$4 last; -rewrite "^/article_cat-([0-9]+)-([0-9]+)(.*)\.html$" /article_cat.php?id=$1&page=$2 last; -rewrite "^/article_cat-([0-9]+)(.*)\.html$" /article_cat.php?id=$1 last; -rewrite "^/article-([0-9]+)(.*)\.html$" /article.php?id=$1 last; -rewrite "^/brand-([0-9]+)-c([0-9]+)-([0-9]+)-(.+)-([a-zA-Z]+)\.html" /brand.php?id=$1&cat=$2&page=$3&sort=$4&order=$5 last; -rewrite "^/brand-([0-9]+)-c([0-9]+)-([0-9]+)(.*)\.html" /brand.php?id=$1&cat=$2&page=$3 last; -rewrite "^/brand-([0-9]+)-c([0-9]+)(.*)\.html" /brand.php?id=$1&cat=$2 last; -rewrite "^/brand-([0-9]+)(.*)\.html" /brand.php?id=$1 last; -rewrite "^/tag-(.*)\.html" /search.php?keywords=$1 last; -rewrite "^/snatch-([0-9]+)\.html$" /snatch.php?id=$1 last; -rewrite "^/group_buy-([0-9]+)\.html$" /group_buy.php?act=view&id=$1 last; -rewrite "^/auction-([0-9]+)\.html$" /auction.php?act=view&id=$1 last; -rewrite "^/exchange-id([0-9]+)(.*)\.html$" /exchange.php?id=$1&act=view last; -rewrite "^/exchange-([0-9]+)-min([0-9]+)-max([0-9]+)-([0-9]+)-(.+)-([a-zA-Z]+)(.*)\.html$" /exchange.php?cat_id=$1&integral_min=$2&integral_max=$3&page=$4&sort=$5&order=$6 last; -rewrite ^/exchange-([0-9]+)-([0-9]+)-(.+)-([a-zA-Z]+)(.*)\.html$" /exchange.php?cat_id=$1&page=$2&sort=$3&order=$4 last; -rewrite "^/exchange-([0-9]+)-([0-9]+)(.*)\.html$" /exchange.php?cat_id=$1&page=$2 last; -rewrite "^/exchange-([0-9]+)(.*)\.html$" /exchange.php?cat_id=$1 last; -} \ No newline at end of file diff --git a/cmd/server/nginx_conf/rewrite/edusoho.conf b/cmd/server/nginx_conf/rewrite/edusoho.conf deleted file mode 100644 index b0099dcff..000000000 --- a/cmd/server/nginx_conf/rewrite/edusoho.conf +++ /dev/null @@ -1,8 +0,0 @@ -location / { - index app.php; - try_files $uri @rewriteapp; -} - -location @rewriteapp { - rewrite ^(.*)$ /app.php/$1 last; -} \ No newline at end of file diff --git a/cmd/server/nginx_conf/rewrite/emlog.conf b/cmd/server/nginx_conf/rewrite/emlog.conf deleted file mode 100644 index e122a854d..000000000 --- a/cmd/server/nginx_conf/rewrite/emlog.conf +++ /dev/null @@ -1,7 +0,0 @@ -location / { -index index.php index.html; - if (!-e $request_filename) - { - rewrite ^/(.*)$ /index.php last; - } -} \ No newline at end of file diff --git a/cmd/server/nginx_conf/rewrite/empirecms.conf b/cmd/server/nginx_conf/rewrite/empirecms.conf deleted file mode 100644 index c68b92d85..000000000 --- a/cmd/server/nginx_conf/rewrite/empirecms.conf +++ /dev/null @@ -1,8 +0,0 @@ -rewrite ^([^\.]*)/listinfo-(.+?)-(.+?)\.html$ $1/e/action/ListInfo/index.php?classid=$2&page=$3 last; -rewrite ^([^\.]*)/showinfo-(.+?)-(.+?)-(.+?)\.html$ $1/e/action/ShowInfo.php?classid=$2&id=$3&page=$4 last; -rewrite ^([^\.]*)/infotype-(.+?)-(.+?)\.html$ $1/e/action/InfoType/index.php?ttid=$2&page=$3 last; -rewrite ^([^\.]*)/tags-(.+?)-(.+?)\.html$ $1/e/tags/index.php?tagname=$2&page=$3 last; -rewrite ^([^\.]*)/comment-(.+?)-(.+?)-(.+?)-(.+?)-(.+?)-(.+?)\.html$ $1/e/pl/index\.php\?doaction=$2&classid=$3&id=$4&page=$5&myorder=$6&tempid=$7 last; -if (!-e $request_filename) { - return 404; -} \ No newline at end of file diff --git a/cmd/server/nginx_conf/rewrite/laravel5.conf b/cmd/server/nginx_conf/rewrite/laravel5.conf deleted file mode 100644 index da4c67474..000000000 --- a/cmd/server/nginx_conf/rewrite/laravel5.conf +++ /dev/null @@ -1,3 +0,0 @@ -location / { - try_files $uri $uri/ /index.php?$query_string; -} \ No newline at end of file diff --git a/cmd/server/nginx_conf/rewrite/maccms.conf b/cmd/server/nginx_conf/rewrite/maccms.conf deleted file mode 100644 index b08325169..000000000 --- a/cmd/server/nginx_conf/rewrite/maccms.conf +++ /dev/null @@ -1,13 +0,0 @@ -if (!-e $request_filename) { - rewrite ^/index.php(.*)$ /index.php?s=$1 break; - # MacCMS要求强制修改后台文件名称 所以需要手动修改下方这条重写规则 将admin修改为你修改后的文件名即可 - rewrite ^/admin.php(.*)$ /admin.php?s=$1 break; - rewrite ^/api.php(.*)$ /api.php?s=$1 break; - rewrite ^/(.*)$ /index.php?s=$1 break; - rewrite ^/vod-(.*)$ /index.php?m=vod-$1 break; - rewrite ^/art-(.*)$ /index.php?m=art-$1 break; - rewrite ^/gbook-(.*)$ /index.php?m=gbook-$1 break; - rewrite ^/label-(.*)$ /index.php?m=label-$1 break; - rewrite ^/map-(.*)$ /index.php?m=map-$1 break; - } -try_files $uri $uri/ /index.php?$query_string; \ No newline at end of file diff --git a/cmd/server/nginx_conf/rewrite/mvc.conf b/cmd/server/nginx_conf/rewrite/mvc.conf deleted file mode 100644 index bf906257b..000000000 --- a/cmd/server/nginx_conf/rewrite/mvc.conf +++ /dev/null @@ -1,6 +0,0 @@ -location /{ - if (!-e $request_filename) { - rewrite ^(.*)$ /index.php/$1 last; - break; - } -} diff --git a/cmd/server/nginx_conf/rewrite/niushop.conf b/cmd/server/nginx_conf/rewrite/niushop.conf deleted file mode 100644 index c32c40c71..000000000 --- a/cmd/server/nginx_conf/rewrite/niushop.conf +++ /dev/null @@ -1,6 +0,0 @@ -location / { - if (!-e $request_filename) { - rewrite ^(.*)$ /index.php?s=$1 last; - break; - } -} \ No newline at end of file diff --git a/cmd/server/nginx_conf/rewrite/phpcms.conf b/cmd/server/nginx_conf/rewrite/phpcms.conf deleted file mode 100644 index a6e0df346..000000000 --- a/cmd/server/nginx_conf/rewrite/phpcms.conf +++ /dev/null @@ -1,9 +0,0 @@ -location / { - ###以下为PHPCMS 伪静态化rewrite法则 - rewrite ^(.*)show-([0-9]+)-([0-9]+)\.html$ $1/show.php?itemid=$2&page=$3; - rewrite ^(.*)list-([0-9]+)-([0-9]+)\.html$ $1/list.php?catid=$2&page=$3; - rewrite ^(.*)show-([0-9]+)\.html$ $1/show.php?specialid=$2; - ####以下为PHPWind 伪静态化rewrite法则 - rewrite ^(.*)-htm-(.*)$ $1.php?$2 last; - rewrite ^(.*)/simple/([a-z0-9\_]+\.html)$ $1/simple/index.php?$2 last; -} \ No newline at end of file diff --git a/cmd/server/nginx_conf/rewrite/phpwind.conf b/cmd/server/nginx_conf/rewrite/phpwind.conf deleted file mode 100644 index 388af90d3..000000000 --- a/cmd/server/nginx_conf/rewrite/phpwind.conf +++ /dev/null @@ -1,4 +0,0 @@ -location / { - rewrite ^(.*)-htm-(.*)$ $1.php?$2 last; - rewrite ^(.*)/simple/([a-z0-9\_]+\.html)$ $1/simple/index.php?$2 last; - } \ No newline at end of file diff --git a/cmd/server/nginx_conf/rewrite/sablog.conf b/cmd/server/nginx_conf/rewrite/sablog.conf deleted file mode 100644 index fa4f00c49..000000000 --- a/cmd/server/nginx_conf/rewrite/sablog.conf +++ /dev/null @@ -1,16 +0,0 @@ -location / { - rewrite "^/date/([0-9]{6})/?([0-9]+)?/?$" /index.php?action=article&setdate=$1&page=$2 last; - rewrite ^/page/([0-9]+)?/?$ /index.php?action=article&page=$1 last; - rewrite ^/category/([0-9]+)/?([0-9]+)?/?$ /index.php?action=article&cid=$1&page=$2 last; - rewrite ^/category/([^/]+)/?([0-9]+)?/?$ /index.php?action=article&curl=$1&page=$2 last; - rewrite ^/(archives|search|article|links)/?$ /index.php?action=$1 last; - rewrite ^/(comments|tagslist|trackbacks|article)/?([0-9]+)?/?$ /index.php?action=$1&page=$2 last; - rewrite ^/tag/([^/]+)/?([0-9]+)?/?$ /index.php?action=article&item=$1&page=$2 last; - rewrite ^/archives/([0-9]+)/?([0-9]+)?/?$ /index.php?action=show&id=$1&page=$2 last; - rewrite ^/rss/([0-9]+)?/?$ /rss.php?cid=$1 last; - rewrite ^/rss/([^/]+)/?$ /rss.php?url=$1 last; - rewrite ^/uid/([0-9]+)/?([0-9]+)?/?$ /index.php?action=article&uid=$1&page=$2 last; - rewrite ^/user/([^/]+)/?([0-9]+)?/?$ /index.php?action=article&user=$1&page=$2 last; - rewrite sitemap.xml sitemap.php last; - rewrite ^(.*)/([0-9a-zA-Z\-\_]+)/?([0-9]+)?/?$ $1/index.php?action=show&alias=$2&page=$3 last; -} \ No newline at end of file diff --git a/cmd/server/nginx_conf/rewrite/seacms.conf b/cmd/server/nginx_conf/rewrite/seacms.conf deleted file mode 100644 index 0dc6f3612..000000000 --- a/cmd/server/nginx_conf/rewrite/seacms.conf +++ /dev/null @@ -1,11 +0,0 @@ -location / { - rewrite ^/frim/index(.+?)\.html$ /list/index.php?$1 last; - rewrite ^/movie/index(.+?)\.html$ /detail/index.php?$1 last; - rewrite ^/play/([0-9]+)-([0-9]+)-([0-9]+)\.html$ /video/index.php?$1-$2-$3 last; - rewrite ^/topic/index(.+?)\.html$ /topic/index.php?$1 last; - rewrite ^/topiclist/index(.+?).html$ /topiclist/index.php?$1 last; - rewrite ^/index\.html$ index.php permanent; - rewrite ^/news\.html$ news/ permanent; - rewrite ^/part/index(.+?)\.html$ /articlelist/index.php?$1 last; - rewrite ^/article/index(.+?)\.html$ /article/index.php?$1 last; -} \ No newline at end of file diff --git a/cmd/server/nginx_conf/rewrite/shopex.conf b/cmd/server/nginx_conf/rewrite/shopex.conf deleted file mode 100644 index f57463c1a..000000000 --- a/cmd/server/nginx_conf/rewrite/shopex.conf +++ /dev/null @@ -1,5 +0,0 @@ -location / { -if (!-e $request_filename) { -rewrite ^/(.+\.(html|xml|json|htm|php|jsp|asp|shtml))$ /index.php?$1 last; -} -} \ No newline at end of file diff --git a/cmd/server/nginx_conf/rewrite/shopwind.conf b/cmd/server/nginx_conf/rewrite/shopwind.conf deleted file mode 100644 index 0edf086c3..000000000 --- a/cmd/server/nginx_conf/rewrite/shopwind.conf +++ /dev/null @@ -1,14 +0,0 @@ -location / { - #Redirect everything that isn't a real file to index.php - try_files $uri $uri/ /index.php$is_args$args; -} -#If you want a single domain name at the front and back ends -location /admin { - try_files $uri $uri/ /admin/index.php$is_args$args; -} -location /mobile { - try_files $uri $uri/ /mobile/index.php$is_args$args; -} -location /api { - try_files $uri $uri/ /api/index.php$is_args$args; -} \ No newline at end of file diff --git a/cmd/server/nginx_conf/rewrite/thinkphp.conf b/cmd/server/nginx_conf/rewrite/thinkphp.conf deleted file mode 100644 index 216b1dc23..000000000 --- a/cmd/server/nginx_conf/rewrite/thinkphp.conf +++ /dev/null @@ -1,8 +0,0 @@ -location ~* (runtime|application)/ { - return 403; -} -location / { - if (!-e $request_filename){ - rewrite ^(.*)$ /index.php?s=$1 last; break; - } -} \ No newline at end of file diff --git a/cmd/server/nginx_conf/rewrite/typecho.conf b/cmd/server/nginx_conf/rewrite/typecho.conf deleted file mode 100644 index dae6ba9cc..000000000 --- a/cmd/server/nginx_conf/rewrite/typecho.conf +++ /dev/null @@ -1,3 +0,0 @@ - if (!-e $request_filename) { - rewrite ^(.*)$ /index.php$1 last; - } diff --git a/cmd/server/nginx_conf/rewrite/typecho2.conf b/cmd/server/nginx_conf/rewrite/typecho2.conf deleted file mode 100644 index 22397d847..000000000 --- a/cmd/server/nginx_conf/rewrite/typecho2.conf +++ /dev/null @@ -1,5 +0,0 @@ -location /typecho/ { - if (!-e $request_filename) { - rewrite ^(.*)$ /typecho/index.php$1 last; - } -} diff --git a/cmd/server/nginx_conf/rewrite/wordpress.conf b/cmd/server/nginx_conf/rewrite/wordpress.conf deleted file mode 100644 index 9fe73c1eb..000000000 --- a/cmd/server/nginx_conf/rewrite/wordpress.conf +++ /dev/null @@ -1,6 +0,0 @@ -location / -{ - try_files $uri $uri/ /index.php?$args; -} - -rewrite /wp-admin$ $scheme://$host$uri/ permanent; \ No newline at end of file diff --git a/cmd/server/nginx_conf/rewrite/wp2.conf b/cmd/server/nginx_conf/rewrite/wp2.conf deleted file mode 100644 index 0e5fbaede..000000000 --- a/cmd/server/nginx_conf/rewrite/wp2.conf +++ /dev/null @@ -1,6 +0,0 @@ -rewrite ^.*/files/(.*)$ /wp-includes/ms-files.php?file=$1 last; -if (!-e $request_filename){ - rewrite ^.+?(/wp-.*) $1 last; - rewrite ^.+?(/.*\.php)$ $1 last; - rewrite ^ /index.php last; -} \ No newline at end of file diff --git a/cmd/server/nginx_conf/rewrite/yii2.conf b/cmd/server/nginx_conf/rewrite/yii2.conf deleted file mode 100644 index 103ebdc7d..000000000 --- a/cmd/server/nginx_conf/rewrite/yii2.conf +++ /dev/null @@ -1,3 +0,0 @@ -location / { - try_files $uri $uri/ /index.php$is_args$args; -} \ No newline at end of file diff --git a/cmd/server/nginx_conf/rewrite/zblog.conf b/cmd/server/nginx_conf/rewrite/zblog.conf deleted file mode 100644 index 5d2de2b79..000000000 --- a/cmd/server/nginx_conf/rewrite/zblog.conf +++ /dev/null @@ -1,9 +0,0 @@ -if (-f $request_filename/index.html){ - rewrite (.*) $1/index.html break; -} -if (-f $request_filename/index.php){ - rewrite (.*) $1/index.php; -} -if (!-f $request_filename){ - rewrite (.*) /index.php; -} \ No newline at end of file diff --git a/cmd/server/nginx_conf/ssl.conf b/cmd/server/nginx_conf/ssl.conf deleted file mode 100644 index 908c52fb4..000000000 --- a/cmd/server/nginx_conf/ssl.conf +++ /dev/null @@ -1,9 +0,0 @@ -ssl_certificate /www/server/panel/vhost/cert/1panel.cloud/fullchain.pem; -ssl_certificate_key /www/server/panel/vhost/cert/1panel.cloud/privkey.pem; -ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; -ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK:!KRB5:!SRP:!CAMELLIA:!SEED; -ssl_prefer_server_ciphers on; -ssl_session_cache shared:SSL:10m; -ssl_session_timeout 10m; -error_page 497 https://$host$request_uri; -proxy_set_header X-Forwarded-Proto https; \ No newline at end of file diff --git a/cmd/server/nginx_conf/stop.html b/cmd/server/nginx_conf/stop.html deleted file mode 100644 index a38fa64b6..000000000 --- a/cmd/server/nginx_conf/stop.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - 抱歉,站点已暂停 - - - - -
-

抱歉!该站点已经被管理员停止运行,请联系管理员了解详情!

-
- - \ No newline at end of file diff --git a/cmd/server/nginx_conf/website_default.conf b/cmd/server/nginx_conf/website_default.conf deleted file mode 100644 index 57c439312..000000000 --- a/cmd/server/nginx_conf/website_default.conf +++ /dev/null @@ -1,23 +0,0 @@ -server { - listen 80; - server_name ko.wp-1.com; - - index index.php index.html index.htm default.php default.htm default.html; - - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Host $server_name; - proxy_set_header X-Real-IP $remote_addr; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - - access_log /www/sites/domain/log/access.log main; - error_log /www/sites/domain/log/error.log; - - location ^~ /.well-known/acme-challenge { - allow all; - root /usr/share/nginx/html; - } -} diff --git a/core/cmd/server/web/favicon.png b/cmd/server/web/favicon.png similarity index 100% rename from core/cmd/server/web/favicon.png rename to cmd/server/web/favicon.png diff --git a/core/cmd/server/web/web.go b/cmd/server/web/web.go similarity index 100% rename from core/cmd/server/web/web.go rename to cmd/server/web/web.go diff --git a/core/cmd/server/conf/app.yaml b/core/cmd/server/conf/app.yaml deleted file mode 100644 index 61c29db47..000000000 --- a/core/cmd/server/conf/app.yaml +++ /dev/null @@ -1,18 +0,0 @@ -system: - db_core_file: 1Panel_Core.db - db_file: 1Panel_Core.db - base_dir: /opt - mode: dev - repo_url: https://resource.fit2cloud.com/1panel/package - app_repo: https://apps-assets.fit2cloud.com - is_demo: false - port: 9999 - username: admin - password: admin123 - -log: - level: debug - time_zone: Asia/Shanghai - log_name: 1Panel - log_suffix: .log - max_backup: 10 diff --git a/core/cmd/server/conf/conf.go b/core/cmd/server/conf/conf.go deleted file mode 100644 index 6654d8bcf..000000000 --- a/core/cmd/server/conf/conf.go +++ /dev/null @@ -1,6 +0,0 @@ -package conf - -import _ "embed" - -//go:embed app.yaml -var AppYaml []byte diff --git a/core/cmd/server/conf/qqwry/qqwey.go b/core/cmd/server/conf/qqwry/qqwey.go deleted file mode 100644 index 94ee4abb8..000000000 --- a/core/cmd/server/conf/qqwry/qqwey.go +++ /dev/null @@ -1,6 +0,0 @@ -package qqwry - -import _ "embed" - -//go:embed qqwry.dat -var QQwryByte []byte diff --git a/core/cmd/server/conf/qqwry/qqwry.dat b/core/cmd/server/conf/qqwry/qqwry.dat deleted file mode 100644 index 0752ec83133c38de1066aef8f230152b60e2bd6b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11407867 zcmZ^s3w%_?+5gWji9t~V-lC!=iWX2YRa8`}YXpo`B}h_ zZ>rYfr8aU6_uR9&vdQLt-CQ6+6ayj#MGO!$KqT}3{brt%CD1>gkACx;dFGkxnRCvZ zIcKN8yVyE9Y>TCovL?q&iLo+Pl^)I6(dM=A&rw#Af5K|6s^3_+rgTHYF@I-MMPOxH zQ&ah<2@BlM&%S>_;-aMDojH3)TMk$|ud=L|sZ%CT_(d=_e`9{;#3}LKzQ)6qJ(hx% z16FE~<;YuAysmbIl>?ua3bP#P<=NdOz0Flt`NrJdjIFWm1+$YD&Rf#j6!f&8QKRW`)2B?Dj?y?Bjz6%T_=i#v&bOov(f{8%h_wpCA4siSpw1Ghi!E#3 zElU<7D~LlDb7BhwE8%dOL{6t8BV+6O+NOe4IR~qbSs59nC)%1Cj*VjIXkTSfQ83LQ zXRugN6O*2D&vh>x^W37OIrCm*pI9oy>1b<0y(UaS<5#b1=-XCV{8i&m(bXHPj>If{ zX;$LmCF~_Mv*bP)9;+SJsXdA&LnQ0r15tFPU<5`@`0X8c+@#w}4Kfy1h{KWUKre!9 zfe`Rz=rGY9ke(E%J%u`)pIcVm=T*mYHzGL9(YhkH+KQhy=Os%e4VIR$FvQkbYR+Is zU~k^%{Vigt`GXyfH=LGQ0KN|{6zvxMzUY66UOw32gq`ym*eZ!rXFL1{OR^h#Fu
  • 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

    jcym(OQg9Flw*XK+IJz_ zwtP*Ye7w)@#+PXH3MgyUJnDC-7lKof$>-ExFn%TVD(Y0~G_Vt9xu4oF-s@JUZUe=s zSa(}Ti|5sqO*M{757FdbOfrG`aj@q=%REUvoALjpej7|i)&I*N@qm}z2 zx}Zn7hO*$%X);ZGj)^vw-;pvA9Crkr}ogZgmh5OVlqH^BxEghCV1CNIJ=N*i3|daD|4wz_1s=cVWMbIt6?Y+EAOGI2&uHY0kaO zZFMTtO?juw+_}56R#h#o{N+2WoW;j;w#lL*`}K=1bGsUl#l_$b$%@+33&pon8*eYQ zxeZ~XPS@;X5hg|Zz@As*c>u(D&d|8e5GHOO28ST-NG}#&*dY6*4O{d;BE=h;_ z<96l4{IpP$n&p*kCLWq}2Y-fXPwLC42T>0W&7yF1-crY@+h`ICr{+g!bNCo`7-4f* z$0PXvmrff02ja(HW0^m}8K6;pW3Rq$Yn|HPB>#6h&xLzImdE6#ty$ApUvs?bo3!e6 zjg9+rzDp}xd$@dk=Y#h_-}#w>a2D*w3LWa4zZ80f9EDM<151=m} zu;ZoOP{id-2%Dvk!X?bckC6#eCnp4teDN@pBa#o41~5p_^X9PnlI+UDU+dT7i3L5v znXBFMW!%kKYgft68{Kk^UAg-5yjlt9JsOwcC(Ry<(~a0`G|M

    J<-5T>7hPmDNXH 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

    -M@Z1+KHbYf3}?_DoMP$>shZRrelnSv5a? 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

    %E)8!B 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 z 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

    XP*51gJ<3T?4yr9a>t`DyjZlc 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