diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo
index 17fc22c3a..5e4c9d63f 100644
--- a/apps/locale/ja/LC_MESSAGES/django.mo
+++ b/apps/locale/ja/LC_MESSAGES/django.mo
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:5d443763c06877304dca8dac76131271287acc6594df665bdf9445455c5187f1
-size 167791
+oid sha256:a00c0d53df7fa88fc2fe69adda31fd9ab581b5a0362a01b8191924f74fab800d
+size 167820
diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po
index df7dee7d5..8dad75a93 100644
--- a/apps/locale/ja/LC_MESSAGES/django.po
+++ b/apps/locale/ja/LC_MESSAGES/django.po
@@ -8,8 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-12-08 14:51+0800\n"
-"POT-Creation-Date: 2023-12-08 15:33+0800\n"
+"POT-Creation-Date: 2023-12-11 14:54+0800\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -88,7 +87,7 @@ msgstr "集めました"
 msgid "Template"
 msgstr "テンプレート"
 
-#: accounts/const/account.py:31 ops/const.py:45
+#: accounts/const/account.py:31 ops/const.py:46
 msgid "Skip"
 msgstr "スキップ"
 
@@ -100,7 +99,7 @@ msgstr "更新"
 #: accounts/const/account.py:33
 #: accounts/serializers/automations/change_secret.py:150 audits/const.py:62
 #: audits/signal_handlers/activity_log.py:33 common/const/choices.py:19
-#: ops/const.py:74 terminal/const.py:79 xpack/plugins/cloud/const.py:46
+#: ops/const.py:75 terminal/const.py:79 xpack/plugins/cloud/const.py:46
 msgid "Failed"
 msgstr "失敗しました"
 
@@ -208,7 +207,6 @@ msgstr "作成のみ"
 msgid "Email"
 msgstr "メール"
 
-#: accounts/const/automation.py:104 terminal/const.py:87
 #: accounts/const/automation.py:105 terminal/const.py:87
 msgid "SFTP"
 msgstr "SFTP"
@@ -265,7 +263,7 @@ msgstr "資産"
 #: accounts/models/account.py:53 accounts/models/template.py:16
 #: accounts/serializers/account/account.py:220
 #: accounts/serializers/account/account.py:268
-#: accounts/serializers/account/template.py:25
+#: accounts/serializers/account/template.py:27
 #: authentication/serializers/connect_token_secret.py:50
 msgid "Su from"
 msgstr "から切り替え"
@@ -397,7 +395,7 @@ msgstr "理由"
 #: accounts/models/automations/backup_account.py:135
 #: accounts/serializers/automations/change_secret.py:105
 #: accounts/serializers/automations/change_secret.py:128
-#: ops/serializers/job.py:55 terminal/serializers/session.py:49
+#: ops/serializers/job.py:65 terminal/serializers/session.py:49
 msgid "Is success"
 msgstr "成功は"
 
@@ -585,7 +583,7 @@ msgstr "ひみつ"
 msgid "Secret strategy"
 msgstr "鍵ポリシー"
 
-#: accounts/models/base.py:44 accounts/serializers/account/template.py:22
+#: accounts/models/base.py:44 accounts/serializers/account/template.py:24
 #: accounts/serializers/automations/change_secret.py:44
 msgid "Password rules"
 msgstr "パスワードルール"
@@ -902,19 +900,19 @@ msgstr "数値#スウスウ#"
 msgid "Special symbol"
 msgstr "特殊記号"
 
-#: accounts/serializers/account/template.py:18
+#: accounts/serializers/account/template.py:19
 msgid "Exclude symbol"
 msgstr "除外文字"
 
-#: accounts/serializers/account/template.py:36
+#: accounts/serializers/account/template.py:38
 msgid "Secret generation strategy for account creation"
 msgstr "账号创建时,密文生成策略"
 
-#: accounts/serializers/account/template.py:37
+#: accounts/serializers/account/template.py:39
 msgid "Whether to automatically push the account to the asset"
 msgstr "是否自动推送账号到资产"
 
-#: accounts/serializers/account/template.py:40
+#: accounts/serializers/account/template.py:42
 msgid ""
 "Associated platform, you can configure push parameters. If not associated, "
 "default parameters will be used"
@@ -980,7 +978,7 @@ msgstr "自動タスク実行履歴"
 
 #: accounts/serializers/automations/change_secret.py:149 audits/const.py:61
 #: audits/models.py:64 audits/signal_handlers/activity_log.py:33
-#: common/const/choices.py:18 ops/const.py:72 ops/serializers/celery.py:40
+#: common/const/choices.py:18 ops/const.py:73 ops/serializers/celery.py:40
 #: terminal/const.py:78 terminal/models/session/sharing.py:121
 #: tickets/views/approve.py:117
 msgid "Success"
@@ -1118,7 +1116,7 @@ msgid "Accounts"
 msgstr "アカウント"
 
 #: acls/models/command_acl.py:16 assets/models/cmd_filter.py:60
-#: ops/serializers/job.py:54 terminal/const.py:86
+#: ops/serializers/job.py:64 terminal/const.py:86
 #: terminal/models/session/session.py:42 terminal/serializers/command.py:18
 #: terminal/templates/terminal/_msg_command_alert.html:12
 #: terminal/templates/terminal/_msg_command_execute_alert.html:10
@@ -1668,7 +1666,7 @@ msgstr "プロトコル"
 msgid "Sudo"
 msgstr "すど"
 
-#: assets/models/_user.py:55 ops/const.py:49 ops/const.py:59
+#: assets/models/_user.py:55 ops/const.py:50 ops/const.py:60
 msgid "Shell"
 msgstr "シェル"
 
@@ -3639,7 +3637,7 @@ msgstr "の準備を"
 msgid "Pending"
 msgstr "未定"
 
-#: common/const/choices.py:17 ops/const.py:71
+#: common/const/choices.py:17 ops/const.py:72
 msgid "Running"
 msgstr "ランニング"
 
@@ -4053,6 +4051,10 @@ msgstr "タスクは存在しません"
 msgid "Task {} args or kwargs error"
 msgstr "タスク実行パラメータエラー"
 
+#: ops/api/job.py:128
+msgid "Duplicate file exists"
+msgstr "重複したファイルが存在する"
+
 #: ops/api/playbook.py:39
 msgid "Currently playbook is being used in a job"
 msgstr "現在プレイブックは1つのジョブで使用されています"
@@ -4125,47 +4127,53 @@ msgstr "コマンド#コマンド#"
 msgid "Playbook"
 msgstr "Playbook"
 
-#: ops/const.py:43
+#: ops/const.py:40
+#, fuzzy
+#| msgid "Upload"
+msgid "Upload File"
+msgstr "アップロード"
+
+#: ops/const.py:44
 msgid "Privileged Only"
 msgstr "特権アカウントのみ"
 
-#: ops/const.py:44
+#: ops/const.py:45
 msgid "Privileged First"
 msgstr "特権アカウント優先"
 
-#: ops/const.py:50 ops/const.py:60
+#: ops/const.py:51 ops/const.py:61
 msgid "Powershell"
 msgstr "PowerShell"
 
-#: ops/const.py:51 ops/const.py:61
+#: ops/const.py:52 ops/const.py:62
 msgid "Python"
 msgstr "Python"
 
-#: ops/const.py:52 ops/const.py:62
+#: ops/const.py:53 ops/const.py:63
 msgid "MySQL"
 msgstr "MySQL"
 
-#: ops/const.py:53 ops/const.py:64
+#: ops/const.py:54 ops/const.py:65
 msgid "PostgreSQL"
 msgstr "PostgreSQL"
 
-#: ops/const.py:54 ops/const.py:65
+#: ops/const.py:55 ops/const.py:66
 msgid "SQLServer"
 msgstr "SQLServer"
 
-#: ops/const.py:55 ops/const.py:67
+#: ops/const.py:56 ops/const.py:68
 msgid "Raw"
 msgstr ""
 
-#: ops/const.py:63
+#: ops/const.py:64
 msgid "MariaDB"
 msgstr "MariaDB"
 
-#: ops/const.py:66
+#: ops/const.py:67
 msgid "Oracle"
 msgstr "Oracle"
 
-#: ops/const.py:73
+#: ops/const.py:74
 msgid "Timeout"
 msgstr "タイムアウト"
 
@@ -4299,7 +4307,7 @@ msgstr "Material"
 msgid "Material Type"
 msgstr "Material を選択してオプションを設定します。"
 
-#: ops/models/job.py:557
+#: ops/models/job.py:565
 msgid "Job Execution"
 msgstr "ジョブ実行"
 
@@ -4343,15 +4351,15 @@ msgstr "{max_threshold} を超えるCPUロード: => {value}"
 msgid "Run after save"
 msgstr "保存後に実行"
 
-#: ops/serializers/job.py:53
+#: ops/serializers/job.py:63
 msgid "Job type"
 msgstr "タスクの種類"
 
-#: ops/serializers/job.py:56 terminal/serializers/session.py:53
+#: ops/serializers/job.py:66 terminal/serializers/session.py:53
 msgid "Is finished"
 msgstr "終了しました"
 
-#: ops/serializers/job.py:57
+#: ops/serializers/job.py:67
 msgid "Time cost"
 msgstr "時を過ごす"
 
@@ -4567,7 +4575,8 @@ msgstr "認定アカウント"
 msgid "today"
 msgstr "今日"
 
-#: perms/notifications.py:12 settings/serializers/feature.py:106
+#: perms/notifications.py:12 perms/notifications.py:44
+#: settings/serializers/feature.py:106
 msgid "day"
 msgstr "日"
 
@@ -4595,8 +4604,9 @@ msgstr "アセット認証ルールの有効期限が切れていることを確
 msgid "Send asset permission expired notification"
 msgstr "アセット許可の有効期限通知を送信する"
 
-
+#: perms/templates/perms/_msg_item_permissions_expire.html:7
 #: perms/templates/perms/_msg_permed_items_expire.html:7
+#, python-format
 msgid ""
 "\n"
 "        The following %(item_type)s will expire in %(count)s\n"
diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo
index 4de90d618..16c60122c 100644
--- a/apps/locale/zh/LC_MESSAGES/django.mo
+++ b/apps/locale/zh/LC_MESSAGES/django.mo
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:8976c6b41e2c0ce591b2b257fd0352b4ed7517661f7df83ba74b302b8cf94b00
-size 137479
+oid sha256:d4a9a61bf1b247d3843001737ebfa6d5f8580f2d9c1ae0fc76649ecc535f5d96
+size 137563
diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po
index a34de449a..29b3e9c50 100644
--- a/apps/locale/zh/LC_MESSAGES/django.po
+++ b/apps/locale/zh/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: JumpServer 0.3.3\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-12-08 15:33+0800\n"
+"POT-Creation-Date: 2023-12-11 14:54+0800\n"
 "PO-Revision-Date: 2021-05-20 10:54+0800\n"
 "Last-Translator: ibuler <ibuler@qq.com>\n"
 "Language-Team: JumpServer team<ibuler@qq.com>\n"
@@ -86,7 +86,7 @@ msgstr "收集"
 msgid "Template"
 msgstr "模板"
 
-#: accounts/const/account.py:31 ops/const.py:45
+#: accounts/const/account.py:31 ops/const.py:46
 msgid "Skip"
 msgstr "跳过"
 
@@ -98,7 +98,7 @@ msgstr "更新"
 #: accounts/const/account.py:33
 #: accounts/serializers/automations/change_secret.py:150 audits/const.py:62
 #: audits/signal_handlers/activity_log.py:33 common/const/choices.py:19
-#: ops/const.py:74 terminal/const.py:79 xpack/plugins/cloud/const.py:46
+#: ops/const.py:75 terminal/const.py:79 xpack/plugins/cloud/const.py:46
 msgid "Failed"
 msgstr "失败"
 
@@ -262,7 +262,7 @@ msgstr "资产"
 #: accounts/models/account.py:53 accounts/models/template.py:16
 #: accounts/serializers/account/account.py:220
 #: accounts/serializers/account/account.py:268
-#: accounts/serializers/account/template.py:25
+#: accounts/serializers/account/template.py:27
 #: authentication/serializers/connect_token_secret.py:50
 msgid "Su from"
 msgstr "切换自"
@@ -394,7 +394,7 @@ msgstr "原因"
 #: accounts/models/automations/backup_account.py:135
 #: accounts/serializers/automations/change_secret.py:105
 #: accounts/serializers/automations/change_secret.py:128
-#: ops/serializers/job.py:55 terminal/serializers/session.py:49
+#: ops/serializers/job.py:65 terminal/serializers/session.py:49
 msgid "Is success"
 msgstr "是否成功"
 
@@ -582,7 +582,7 @@ msgstr "密钥"
 msgid "Secret strategy"
 msgstr "密文策略"
 
-#: accounts/models/base.py:44 accounts/serializers/account/template.py:22
+#: accounts/models/base.py:44 accounts/serializers/account/template.py:24
 #: accounts/serializers/automations/change_secret.py:44
 msgid "Password rules"
 msgstr "密码规则"
@@ -898,19 +898,19 @@ msgstr "数字"
 msgid "Special symbol"
 msgstr "特殊字符"
 
-#: accounts/serializers/account/template.py:18
+#: accounts/serializers/account/template.py:19
 msgid "Exclude symbol"
 msgstr "排除字符"
 
-#: accounts/serializers/account/template.py:36
+#: accounts/serializers/account/template.py:38
 msgid "Secret generation strategy for account creation"
 msgstr "密码生成策略,用于账号创建时,设置密码"
 
-#: accounts/serializers/account/template.py:37
+#: accounts/serializers/account/template.py:39
 msgid "Whether to automatically push the account to the asset"
 msgstr "是否自动推送账号到资产"
 
-#: accounts/serializers/account/template.py:40
+#: accounts/serializers/account/template.py:42
 msgid ""
 "Associated platform, you can configure push parameters. If not associated, "
 "default parameters will be used"
@@ -975,7 +975,7 @@ msgstr "自动化任务执行历史"
 
 #: accounts/serializers/automations/change_secret.py:149 audits/const.py:61
 #: audits/models.py:64 audits/signal_handlers/activity_log.py:33
-#: common/const/choices.py:18 ops/const.py:72 ops/serializers/celery.py:40
+#: common/const/choices.py:18 ops/const.py:73 ops/serializers/celery.py:40
 #: terminal/const.py:78 terminal/models/session/sharing.py:121
 #: tickets/views/approve.py:117
 msgid "Success"
@@ -1113,7 +1113,7 @@ msgid "Accounts"
 msgstr "账号管理"
 
 #: acls/models/command_acl.py:16 assets/models/cmd_filter.py:60
-#: ops/serializers/job.py:54 terminal/const.py:86
+#: ops/serializers/job.py:64 terminal/const.py:86
 #: terminal/models/session/session.py:42 terminal/serializers/command.py:18
 #: terminal/templates/terminal/_msg_command_alert.html:12
 #: terminal/templates/terminal/_msg_command_execute_alert.html:10
@@ -1659,7 +1659,7 @@ msgstr "协议"
 msgid "Sudo"
 msgstr "Sudo"
 
-#: assets/models/_user.py:55 ops/const.py:49 ops/const.py:59
+#: assets/models/_user.py:55 ops/const.py:50 ops/const.py:60
 msgid "Shell"
 msgstr "Shell"
 
@@ -3594,7 +3594,7 @@ msgstr "准备"
 msgid "Pending"
 msgstr "待定的"
 
-#: common/const/choices.py:17 ops/const.py:71
+#: common/const/choices.py:17 ops/const.py:72
 msgid "Running"
 msgstr "运行中"
 
@@ -4003,6 +4003,10 @@ msgstr "任务 {} 不存在"
 msgid "Task {} args or kwargs error"
 msgstr "任务 {} 执行参数错误"
 
+#: ops/api/job.py:128
+msgid "Duplicate file exists"
+msgstr "存在同名文件"
+
 #: ops/api/playbook.py:39
 msgid "Currently playbook is being used in a job"
 msgstr "当前 playbook 正在作业中使用"
@@ -4075,47 +4079,51 @@ msgstr "命令"
 msgid "Playbook"
 msgstr "Playbook"
 
-#: ops/const.py:43
+#: ops/const.py:40
+msgid "Upload File"
+msgstr "上传"
+
+#: ops/const.py:44
 msgid "Privileged Only"
 msgstr "仅限特权账号"
 
-#: ops/const.py:44
+#: ops/const.py:45
 msgid "Privileged First"
 msgstr "特权账号优先"
 
-#: ops/const.py:50 ops/const.py:60
+#: ops/const.py:51 ops/const.py:61
 msgid "Powershell"
 msgstr "PowerShell"
 
-#: ops/const.py:51 ops/const.py:61
+#: ops/const.py:52 ops/const.py:62
 msgid "Python"
 msgstr "Python"
 
-#: ops/const.py:52 ops/const.py:62
+#: ops/const.py:53 ops/const.py:63
 msgid "MySQL"
 msgstr "MySQL"
 
-#: ops/const.py:53 ops/const.py:64
+#: ops/const.py:54 ops/const.py:65
 msgid "PostgreSQL"
 msgstr "PostgreSQL"
 
-#: ops/const.py:54 ops/const.py:65
+#: ops/const.py:55 ops/const.py:66
 msgid "SQLServer"
 msgstr "SQLServer"
 
-#: ops/const.py:55 ops/const.py:67
+#: ops/const.py:56 ops/const.py:68
 msgid "Raw"
 msgstr "Raw"
 
-#: ops/const.py:63
+#: ops/const.py:64
 msgid "MariaDB"
 msgstr "MariaDB"
 
-#: ops/const.py:66
+#: ops/const.py:67
 msgid "Oracle"
 msgstr "Oracle"
 
-#: ops/const.py:73
+#: ops/const.py:74
 msgid "Timeout"
 msgstr "超时"
 
@@ -4249,7 +4257,7 @@ msgstr "Material"
 msgid "Material Type"
 msgstr "Material 类型"
 
-#: ops/models/job.py:557
+#: ops/models/job.py:565
 msgid "Job Execution"
 msgstr "作业执行"
 
@@ -4293,15 +4301,15 @@ msgstr "CPU 使用率超过 {max_threshold}: => {value}"
 msgid "Run after save"
 msgstr "保存后执行"
 
-#: ops/serializers/job.py:53
+#: ops/serializers/job.py:63
 msgid "Job type"
 msgstr "任务类型"
 
-#: ops/serializers/job.py:56 terminal/serializers/session.py:53
+#: ops/serializers/job.py:66 terminal/serializers/session.py:53
 msgid "Is finished"
 msgstr "是否完成"
 
-#: ops/serializers/job.py:57
+#: ops/serializers/job.py:67
 msgid "Time cost"
 msgstr "花费时间"
 
@@ -4516,7 +4524,8 @@ msgstr "授权账号"
 msgid "today"
 msgstr "今天"
 
-#: perms/notifications.py:12 settings/serializers/feature.py:106
+#: perms/notifications.py:12 perms/notifications.py:44
+#: settings/serializers/feature.py:106
 msgid "day"
 msgstr "天"
 
@@ -4545,20 +4554,6 @@ msgid "Send asset permission expired notification"
 msgstr "发送资产权限过期通知"
 
 #: perms/templates/perms/_msg_item_permissions_expire.html:7
-#, fuzzy, python-format
-#| msgid ""
-#| "\n"
-#| "        The following %(item_type)s will expire in %(count)s\n"
-#| "    "
-msgid ""
-"\n"
-"        The following %(item_type)s will expire in %(count)s days\n"
-"    "
-msgstr ""
-"\n"
-"        以下 %(item_type)s 即将在 %(count)s 后过期\n"
-"    "
-
 #: perms/templates/perms/_msg_permed_items_expire.html:7
 #, python-format
 msgid ""
@@ -8801,6 +8796,7 @@ msgstr "企业专业版"
 msgid "Ultimate edition"
 msgstr "企业旗舰版"
 
+
 #~ msgid "FeiShu query user failed"
 #~ msgstr "飞书查询用户失败"
 
diff --git a/apps/ops/ansible/runner.py b/apps/ops/ansible/runner.py
index 4610ddfd2..47af111e5 100644
--- a/apps/ops/ansible/runner.py
+++ b/apps/ops/ansible/runner.py
@@ -1,8 +1,9 @@
 import os
 import uuid
-
+import shutil
 import ansible_runner
 from django.conf import settings
+from django.utils._os import safe_join
 
 from .callback import DefaultCallback
 from ..utils import get_ansible_log_verbosity
@@ -85,6 +86,34 @@ class PlaybookRunner:
         return self.cb
 
 
+class UploadFileRunner:
+    def __init__(self, inventory, job_id, dest_path, callback=None):
+        self.id = uuid.uuid4()
+        self.inventory = inventory
+        self.cb = DefaultCallback()
+        upload_file_dir = safe_join(settings.DATA_DIR, 'job_upload_file')
+        self.src_paths = safe_join(upload_file_dir, str(job_id))
+        self.dest_path = dest_path
+
+    def run(self, verbosity=0, **kwargs):
+        verbosity = get_ansible_log_verbosity(verbosity)
+        ansible_runner.run(
+            host_pattern="*",
+            inventory=self.inventory,
+            module='copy',
+            module_args=f"src={self.src_paths}/ dest={self.dest_path}",
+            verbosity=verbosity,
+            event_handler=self.cb.event_handler,
+            status_handler=self.cb.status_handler,
+            **kwargs
+        )
+        try:
+            shutil.rmtree(self.src_paths)
+        except OSError as e:
+            print(f"del upload tmp dir {self.src_paths} failed! {e}")
+        return self.cb
+
+
 class CommandRunner(AdHocRunner):
     def __init__(self, inventory, command, pattern='*', project_dir='/tmp/'):
         super().__init__(inventory, 'shell', command, pattern, project_dir)
diff --git a/apps/ops/api/job.py b/apps/ops/api/job.py
index f2bf92cae..3bcb7393e 100644
--- a/apps/ops/api/job.py
+++ b/apps/ops/api/job.py
@@ -1,16 +1,22 @@
+import json
+import os
 from django.conf import settings
 from django.db import transaction
 from django.db.models import Count
-from django.db.transaction import atomic
 from django.shortcuts import get_object_or_404
+from django.utils._os import safe_join
+from django.utils.translation import gettext_lazy as _
+
+from rest_framework.decorators import action
 from rest_framework.response import Response
 from rest_framework.views import APIView
 
 from assets.models import Asset
+from common.const.http import POST
 from common.permissions import IsValidUser
 from ops.const import Types
 from ops.models import Job, JobExecution
-from ops.serializers.job import JobSerializer, JobExecutionSerializer
+from ops.serializers.job import JobSerializer, JobExecutionSerializer, FileSerializer
 
 __all__ = [
     'JobViewSet', 'JobExecutionViewSet', 'JobRunVariableHelpAPIView',
@@ -24,6 +30,7 @@ from orgs.utils import tmp_to_org, get_current_org
 from accounts.models import Account
 from perms.models import PermNode
 from perms.utils import UserPermAssetUtil
+from jumpserver.settings import get_file_md5
 
 
 def set_task_to_serializer_data(serializer, task_id):
@@ -91,6 +98,40 @@ class JobViewSet(OrgBulkModelViewSet):
         transaction.on_commit(
             lambda: run_ops_job_execution.apply_async((str(execution.id),), task_id=str(execution.id)))
 
+    @action(methods=[POST], detail=False, serializer_class=FileSerializer, permission_classes=[IsValidUser, ],
+            url_path='upload')
+    def upload(self, request, *args, **kwargs):
+        serializer = self.get_serializer(data=request.data)
+        if not serializer.is_valid():
+            msg = 'Upload data invalid: {}'.format(serializer.errors)
+            return Response({'msg': msg}, status=400)
+        uploaded_files = request.FILES.getlist('files')
+        job_id = request.data.get('job_id', '')
+        job = get_object_or_404(Job, pk=job_id)
+        job_args = json.loads(job.args)
+        src_path_info = []
+        filename_set = set()
+        same_filenames = []
+        upload_file_dir = safe_join(settings.DATA_DIR, 'job_upload_file')
+        for uploaded_file in uploaded_files:
+            filename = uploaded_file.name
+            saved_path = safe_join(upload_file_dir, f'{job_id}/{filename}')
+            os.makedirs(os.path.dirname(saved_path), exist_ok=True)
+            with open(saved_path, 'wb+') as destination:
+                for chunk in uploaded_file.chunks():
+                    destination.write(chunk)
+            if filename in filename_set:
+                same_filenames.append(filename)
+            filename_set.add(filename)
+            src_path_info.append({'filename': filename, 'md5': get_file_md5(saved_path)})
+        if same_filenames:
+            return Response({'msg': _("Duplicate file exists")}, status=400)
+        job_args['src_path_info'] = src_path_info
+        job.args = json.dumps(job_args)
+        job.save()
+        self.run_job(job, serializer)
+        return Response({'task_id': serializer.data.get('task_id')}, status=201)
+
 
 class JobExecutionViewSet(OrgBulkModelViewSet):
     serializer_class = JobExecutionSerializer
diff --git a/apps/ops/const.py b/apps/ops/const.py
index 7fa636a0f..578697c48 100644
--- a/apps/ops/const.py
+++ b/apps/ops/const.py
@@ -37,6 +37,7 @@ class CreateMethods(models.TextChoices):
 class Types(models.TextChoices):
     adhoc = 'adhoc', _('Adhoc')
     playbook = 'playbook', _('Playbook')
+    upload_file = 'upload_file', _('Upload File')
 
 
 class RunasPolicies(models.TextChoices):
diff --git a/apps/ops/models/job.py b/apps/ops/models/job.py
index 7271a3401..358c37136 100644
--- a/apps/ops/models/job.py
+++ b/apps/ops/models/job.py
@@ -23,7 +23,7 @@ from assets.models import Asset
 from assets.automations.base.manager import SSHTunnelManager
 from common.db.encoder import ModelJSONFieldEncoder
 from labels.mixins import LabeledMixin
-from ops.ansible import JMSInventory, AdHocRunner, PlaybookRunner, CommandInBlackListException
+from ops.ansible import JMSInventory, AdHocRunner, PlaybookRunner, CommandInBlackListException, UploadFileRunner
 from ops.mixin import PeriodTaskModelMixin
 from ops.variables import *
 from ops.const import Types, RunasPolicies, JobStatus, JobModules
@@ -362,7 +362,7 @@ class JobExecution(JMSOrgBaseModel):
         static_variables = self.gather_static_variables()
         extra_vars.update(static_variables)
 
-        if self.current_job.type == 'adhoc':
+        if self.current_job.type == Types.adhoc:
             module, args = self.compile_shell()
 
             runner = AdHocRunner(
@@ -374,10 +374,18 @@ class JobExecution(JMSOrgBaseModel):
                 project_dir=self.private_dir,
                 extra_vars=extra_vars,
             )
-        elif self.current_job.type == 'playbook':
+        elif self.current_job.type == Types.playbook:
             runner = PlaybookRunner(
                 self.inventory_path, self.current_job.playbook.entry
             )
+        elif self.current_job.type == Types.upload_file:
+            job_id = self.current_job.id
+            args = json.loads(self.current_job.args)
+            dst_path = args.get('dst_path')
+            if dst_path:
+                runner = UploadFileRunner(self.inventory_path, job_id, dst_path)
+            else:
+                raise ValueError("dst_path is null")
         else:
             raise Exception("unsupported job type")
         return runner
diff --git a/apps/ops/serializers/job.py b/apps/ops/serializers/job.py
index d97a0929a..3bb91d463 100644
--- a/apps/ops/serializers/job.py
+++ b/apps/ops/serializers/job.py
@@ -21,9 +21,12 @@ class JobSerializer(ResourceLabelsMixin, BulkOrgResourceModelSerializer, PeriodT
 
     def to_internal_value(self, data):
         instant = data.get('instant', False)
+        job_type = data.get('type', '')
+        _uid = str(uuid.uuid4()).split('-')[-1]
         if instant:
-            _uid = str(uuid.uuid4()).split('-')[-1]
             data['name'] = f'job-{_uid}'
+        if job_type == 'upload_file':
+            data['name'] = f'upload_file-{_uid}'
         return super().to_internal_value(data)
 
     def get_request_user(self):
@@ -44,10 +47,17 @@ class JobSerializer(ResourceLabelsMixin, BulkOrgResourceModelSerializer, PeriodT
             "use_parameter_define", "parameters_define",
             "timeout", "chdir", "comment", "summary",
             "is_periodic", "interval", "crontab", "nodes",
-            "run_after_save",
+            "run_after_save"
         ]
 
 
+class FileSerializer(serializers.Serializer):
+    files = serializers.FileField(allow_empty_file=True)
+
+    class Meta:
+        ref_name = "JobFileSerializer"
+
+
 class JobExecutionSerializer(BulkOrgResourceModelSerializer):
     creator = ReadableHiddenField(default=serializers.CurrentUserDefault())
     job_type = serializers.ReadOnlyField(label=_("Job type"))
diff --git a/apps/perms/notifications.py b/apps/perms/notifications.py
index b00c50f43..5c82c2589 100644
--- a/apps/perms/notifications.py
+++ b/apps/perms/notifications.py
@@ -41,7 +41,7 @@ class AssetPermsWillExpireForOrgAdminMsg(UserMessage):
         super().__init__(user)
         self.perms = perms
         self.org = org
-        self.day_count = _('today') if day_count == 0 else day_count
+        self.day_count = _('today') if day_count == 0 else day_count + _('day')
 
     def get_items_with_url(self):
         items_with_url = []
diff --git a/apps/perms/templates/perms/_msg_item_permissions_expire.html b/apps/perms/templates/perms/_msg_item_permissions_expire.html
index 9a9dc8244..eba2e7e5b 100644
--- a/apps/perms/templates/perms/_msg_item_permissions_expire.html
+++ b/apps/perms/templates/perms/_msg_item_permissions_expire.html
@@ -5,7 +5,7 @@
 
 <p>
     {% blocktranslate %}
-        The following {{ item_type }} will expire in {{ count }} days
+        The following {{ item_type }} will expire in {{ count }}
     {% endblocktranslate %}
 </p>