Compare commits
10 Commits
703d36038a
...
6350f7a92a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6350f7a92a | ||
|
|
cb8f57c12b | ||
|
|
ae8642cdfe | ||
|
|
45c8623666 | ||
|
|
51eae214cc | ||
|
|
cbe3cfccba | ||
|
|
16a7619ed0 | ||
|
|
5f9d79fbb0 | ||
|
|
bdd2df7e75 | ||
|
|
06c4f8eafa |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -40,4 +40,7 @@ Thumbs.db
|
|||||||
*.xls
|
*.xls
|
||||||
|
|
||||||
# .claude
|
# .claude
|
||||||
.claude/
|
.claude/
|
||||||
|
|
||||||
|
#temp files
|
||||||
|
temp/
|
||||||
57
README.md
57
README.md
@@ -10,28 +10,36 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
claudeskill/
|
claudeskill/
|
||||||
├── .claude/ # Claude 配置目录
|
├── .claude/ # Claude 配置目录(已安装的 Skills)
|
||||||
│ └── skills/ # Skill 创建器引用
|
│ └── skills/ # Skills 创建器引用
|
||||||
│ ├── skill-creator/ # Skill 创建指南和工具
|
│ ├── skill-creator/ # Skill 创建指南和工具
|
||||||
│ ├── excel-to-markdown/ # Excel 转 Markdown Skill
|
│ ├── excel-to-markdown/ # Excel 转 Markdown Skill
|
||||||
│ └── excel-report-converter/ # Excel 报表转换 Skill
|
│ ├── excel-report-converter/ # Excel 报表转换 Skill
|
||||||
├── scripts/ # 独立脚本目录
|
│ └── minio-upload/ # MinIO 文件上传 Skill
|
||||||
│ ├── excel_to_markdown.py # Excel 转 Markdown 转换器
|
├── demo_scripts/ # 演示脚本目录
|
||||||
│ └── report_to_db_format.py # 报表格式转数据库格式脚本
|
│ └── excel_to_markdown.py # Excel 转 Markdown 转换器
|
||||||
├── skills/ # 开发中的 Skills
|
├── skills/ # 开发中的 Skills(用于测试和版本控制)
|
||||||
│ └── excel-report-converter/ # Excel 报表转换器
|
│ ├── excel-to-markdown/ # Excel 转 Markdown Skill
|
||||||
|
│ ├── excel-report-converter/ # Excel 报表转换 Skill
|
||||||
|
│ └── minio-upload/ # MinIO 文件上传 Skill
|
||||||
|
├── temp/ # 临时文件目录
|
||||||
├── .venv/ # Python 虚拟环境
|
├── .venv/ # Python 虚拟环境
|
||||||
|
├── CLAUDE.md # Claude Code 项目指南
|
||||||
└── README.md # 本文档
|
└── README.md # 本文档
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**说明:**
|
||||||
|
- `.claude/skills/` - 已安装的 Skills,由 Claude Code 自动加载(被 gitignore)
|
||||||
|
- `skills/` - Skills 的开发版本,用于版本控制和协作
|
||||||
|
- `demo_scripts/` - 可独立运行的脚本文件
|
||||||
|
|
||||||
## 目录说明
|
## 目录说明
|
||||||
|
|
||||||
### `scripts/`
|
### `demo_scripts/`
|
||||||
存放可独立使用的脚本文件,这些脚本可以被 Skills 引用。
|
存放可独立使用的脚本文件,这些脚本可以被 Skills 引用。
|
||||||
|
|
||||||
**当前脚本:**
|
**当前脚本:**
|
||||||
- `excel_to_markdown.py` - 将 Excel 文件转换为 Markdown 表格格式
|
- `excel_to_markdown.py` - 将 Excel 文件转换为 Markdown 表格格式
|
||||||
- `report_to_db_format.py` - 将报表格式的 Excel 文件转换为数据库记录格式
|
|
||||||
|
|
||||||
### `skills/`
|
### `skills/`
|
||||||
存放已创建的 Skills,每个 Skill 都是一个独立的功能模块。
|
存放已创建的 Skills,每个 Skill 都是一个独立的功能模块。
|
||||||
@@ -69,6 +77,31 @@ claudeskill/
|
|||||||
- 创建自定义的 Excel 报表转换脚本
|
- 创建自定义的 Excel 报表转换脚本
|
||||||
- 将层级报表数据(表头 + 明细行 + 表尾)转换为规范化数据库记录
|
- 将层级报表数据(表头 + 明细行 + 表尾)转换为规范化数据库记录
|
||||||
|
|
||||||
|
#### minio-upload
|
||||||
|
上传文件到 MinIO obsidian 桶的 Skill,支持灵活的路径配置。
|
||||||
|
|
||||||
|
**功能:**
|
||||||
|
- 上传任意文件到 MinIO 的 obsidian 桶
|
||||||
|
- 支持自定义子路径(如 `notes/2024`, `archive/projects`)
|
||||||
|
- 默认上传到 `main` 路径
|
||||||
|
- 自动处理路径规范化和文件名附加
|
||||||
|
- 完善的错误处理和验证
|
||||||
|
|
||||||
|
**使用场景:**
|
||||||
|
- 快速上传 Markdown 文档到云存储
|
||||||
|
- 按年份/分类组织文件
|
||||||
|
- 备份文件到 MinIO
|
||||||
|
- 批量上传文件到指定路径
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
```bash
|
||||||
|
# 上传到默认 main 路径
|
||||||
|
.claude/skills/minio-upload/scripts/upload_to_obsidian.sh file.md
|
||||||
|
|
||||||
|
# 上传到自定义子路径
|
||||||
|
.claude/skills/minio-upload/scripts/upload_to_obsidian.sh file.md "notes/2024"
|
||||||
|
```
|
||||||
|
|
||||||
## 工作流程
|
## 工作流程
|
||||||
|
|
||||||
### 1. 创建新 Skill
|
### 1. 创建新 Skill
|
||||||
@@ -100,7 +133,7 @@ python3 .claude/skills/skill-creator/scripts/init_skill.py <skill-name> --path s
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 测试 Excel 转 Markdown
|
# 测试 Excel 转 Markdown
|
||||||
python3 scripts/excel_to_markdown.py demo.xlsx -o demo.md --show-rows --show-cols
|
python3 demo_scripts/excel_to_markdown.py demo.xlsx -o demo.md --show-rows --show-cols
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 打包 Skill(当用户主动要求才打包)
|
### 4. 打包 Skill(当用户主动要求才打包)
|
||||||
@@ -164,7 +197,7 @@ python3 .claude/skills/skill-creator/scripts/init_skill.py excel-to-markdown --p
|
|||||||
|
|
||||||
### 步骤 2:添加脚本
|
### 步骤 2:添加脚本
|
||||||
```bash
|
```bash
|
||||||
cp excel_to_markdown.py skills/excel-to-markdown/scripts/
|
cp demo_scripts/excel_to_markdown.py skills/excel-to-markdown/scripts/
|
||||||
```
|
```
|
||||||
|
|
||||||
### 步骤 3:编写 SKILL.md
|
### 步骤 3:编写 SKILL.md
|
||||||
|
|||||||
@@ -152,7 +152,8 @@ python3 scripts/custom_converter.py input.xlsx output.xlsx
|
|||||||
Use `excel-to-markdown` to verify the output:
|
Use `excel-to-markdown` to verify the output:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 scripts/excel_to_markdown.py output.xlsx -o /tmp/verify.md --show-rows --show-cols
|
# For large files, only verify first 10 records
|
||||||
|
python3 scripts/excel_to_markdown.py output.xlsx -o /tmp/verify.md --show-rows --show-cols --rows 1:11
|
||||||
```
|
```
|
||||||
|
|
||||||
Read `/tmp/verify.md` and verify:
|
Read `/tmp/verify.md` and verify:
|
||||||
@@ -162,6 +163,8 @@ Read `/tmp/verify.md` and verify:
|
|||||||
4. Footer fields are correctly populated
|
4. Footer fields are correctly populated
|
||||||
5. No data loss or corruption
|
5. No data loss or corruption
|
||||||
|
|
||||||
|
**Important:** For large output files with many records, use `--rows 1:11` to only convert and verify the first 10 data records (plus header row). This avoids processing time and memory issues when verifying large exports.
|
||||||
|
|
||||||
### Step 4: Iterate if Needed
|
### Step 4: Iterate if Needed
|
||||||
|
|
||||||
If verification reveals issues:
|
If verification reveals issues:
|
||||||
|
|||||||
@@ -19,6 +19,18 @@ Convert an entire Excel file to Markdown:
|
|||||||
python3 scripts/excel_to_markdown.py input.xlsx -o output.md
|
python3 scripts/excel_to_markdown.py input.xlsx -o output.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Specify Worksheet
|
||||||
|
|
||||||
|
Convert a specific worksheet by name or index:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# By worksheet name
|
||||||
|
python3 scripts/excel_to_markdown.py input.xlsx -o output.md --sheet "Sheet2"
|
||||||
|
|
||||||
|
# By worksheet index (1-based)
|
||||||
|
python3 scripts/excel_to_markdown.py input.xlsx -o output.md --sheet 2
|
||||||
|
```
|
||||||
|
|
||||||
### Convert with Row/Column Numbers
|
### Convert with Row/Column Numbers
|
||||||
|
|
||||||
Display Excel row and column identifiers for reference:
|
Display Excel row and column identifiers for reference:
|
||||||
@@ -51,6 +63,7 @@ python3 scripts/excel_to_markdown.py input.xlsx -o output.md --start-row 2 --sta
|
|||||||
| `--show-cols` | Display column letters (A, B, C...) in the first row |
|
| `--show-cols` | Display column letters (A, B, C...) in the first row |
|
||||||
| `--start-row` | Starting row number (default: 1) |
|
| `--start-row` | Starting row number (default: 1) |
|
||||||
| `--start-col` | Starting column number (default: 1) |
|
| `--start-col` | Starting column number (default: 1) |
|
||||||
|
| `--sheet` | Worksheet name or index (default: first worksheet) |
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -67,7 +80,12 @@ The converter automatically scans the spreadsheet to identify the actual data ra
|
|||||||
|
|
||||||
### Multi-Sheet Support
|
### Multi-Sheet Support
|
||||||
|
|
||||||
By default, the converter processes the active (last selected) sheet in the workbook.
|
By default, the converter processes the first worksheet in the workbook. You can specify a different worksheet using the `--sheet` parameter:
|
||||||
|
|
||||||
|
- **By name**: `--sheet "Sheet2"` or `--sheet "Data"`
|
||||||
|
- **By index**: `--sheet 2` (1-based indexing)
|
||||||
|
|
||||||
|
If the specified worksheet is not found, an error will display the list of available worksheets.
|
||||||
|
|
||||||
## Common Use Cases
|
## Common Use Cases
|
||||||
|
|
||||||
|
|||||||
@@ -14,15 +14,34 @@ import argparse
|
|||||||
class ExcelToMarkdown:
|
class ExcelToMarkdown:
|
||||||
"""Excel转Markdown转换器"""
|
"""Excel转Markdown转换器"""
|
||||||
|
|
||||||
def __init__(self, excel_file: str):
|
def __init__(self, excel_file: str, sheet_name: Optional[str] = None):
|
||||||
"""
|
"""
|
||||||
初始化转换器
|
初始化转换器
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
excel_file: Excel文件路径
|
excel_file: Excel文件路径
|
||||||
|
sheet_name: 工作表名称或索引(None表示使用第一个工作表)
|
||||||
"""
|
"""
|
||||||
self.workbook = openpyxl.load_workbook(excel_file, data_only=True)
|
self.workbook = openpyxl.load_workbook(excel_file, data_only=True)
|
||||||
self.sheet = self.workbook.active
|
|
||||||
|
# 获取指定的工作表
|
||||||
|
if sheet_name is None:
|
||||||
|
# 使用第一个工作表
|
||||||
|
self.sheet = self.workbook.worksheets[0]
|
||||||
|
elif isinstance(sheet_name, int) or sheet_name.isdigit():
|
||||||
|
# 按索引获取(从1开始)
|
||||||
|
sheet_index = int(sheet_name) - 1
|
||||||
|
if 0 <= sheet_index < len(self.workbook.worksheets):
|
||||||
|
self.sheet = self.workbook.worksheets[sheet_index]
|
||||||
|
else:
|
||||||
|
raise ValueError(f"工作表索引 {sheet_name} 超出范围(1-{len(self.workbook.worksheets)})")
|
||||||
|
else:
|
||||||
|
# 按名称获取
|
||||||
|
if sheet_name in self.workbook.sheetnames:
|
||||||
|
self.sheet = self.workbook[sheet_name]
|
||||||
|
else:
|
||||||
|
available_sheets = ", ".join(self.workbook.sheetnames)
|
||||||
|
raise ValueError(f"找不到工作表 '{sheet_name}',可用的工作表: {available_sheets}")
|
||||||
|
|
||||||
def detect_data_range(self, max_scan_rows: int = 100, max_scan_cols: int = 100) -> Tuple[int, int]:
|
def detect_data_range(self, max_scan_rows: int = 100, max_scan_cols: int = 100) -> Tuple[int, int]:
|
||||||
"""
|
"""
|
||||||
@@ -163,18 +182,24 @@ def main():
|
|||||||
示例:
|
示例:
|
||||||
# 自动检测范围并转换
|
# 自动检测范围并转换
|
||||||
python excel_to_markdown.py input.xlsx -o output.md
|
python excel_to_markdown.py input.xlsx -o output.md
|
||||||
|
|
||||||
# 指定转换前10行、前5列
|
# 指定转换前10行、前5列
|
||||||
python excel_to_markdown.py input.xlsx -o output.md -r 10 -c 5
|
python excel_to_markdown.py input.xlsx -o output.md -r 10 -c 5
|
||||||
|
|
||||||
# 显示行号和列号
|
# 显示行号和列号
|
||||||
python excel_to_markdown.py input.xlsx -o output.md --show-rows --show-cols
|
python excel_to_markdown.py input.xlsx -o output.md --show-rows --show-cols
|
||||||
|
|
||||||
# 从第2行第2列开始,转换5行3列
|
# 从第2行第2列开始,转换5行3列
|
||||||
python excel_to_markdown.py input.xlsx -o output.md -r 5 -c 3 --start-row 2 --start-col 2
|
python excel_to_markdown.py input.xlsx -o output.md -r 5 -c 3 --start-row 2 --start-col 2
|
||||||
|
|
||||||
|
# 指定工作表(按名称)
|
||||||
|
python excel_to_markdown.py input.xlsx -o output.md --sheet "Sheet2"
|
||||||
|
|
||||||
|
# 指定工作表(按索引)
|
||||||
|
python excel_to_markdown.py input.xlsx -o output.md --sheet 2
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument('input_file', help='输入的Excel文件路径')
|
parser.add_argument('input_file', help='输入的Excel文件路径')
|
||||||
parser.add_argument('-o', '--output', help='输出的Markdown文件路径(不指定则输出到控制台)')
|
parser.add_argument('-o', '--output', help='输出的Markdown文件路径(不指定则输出到控制台)')
|
||||||
parser.add_argument('-r', '--max-rows', type=int, help='最大转换行数(不指定则自动检测)')
|
parser.add_argument('-r', '--max-rows', type=int, help='最大转换行数(不指定则自动检测)')
|
||||||
@@ -183,12 +208,13 @@ def main():
|
|||||||
parser.add_argument('--show-cols', action='store_true', help='显示列号(字母形式)')
|
parser.add_argument('--show-cols', action='store_true', help='显示列号(字母形式)')
|
||||||
parser.add_argument('--start-row', type=int, default=1, help='起始行号(默认1)')
|
parser.add_argument('--start-row', type=int, default=1, help='起始行号(默认1)')
|
||||||
parser.add_argument('--start-col', type=int, default=1, help='起始列号(默认1)')
|
parser.add_argument('--start-col', type=int, default=1, help='起始列号(默认1)')
|
||||||
|
parser.add_argument('--sheet', help='工作表名称或索引(不指定则使用第一个工作表)')
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 创建转换器
|
# 创建转换器
|
||||||
converter = ExcelToMarkdown(args.input_file)
|
converter = ExcelToMarkdown(args.input_file, args.sheet)
|
||||||
|
|
||||||
# 执行转换
|
# 执行转换
|
||||||
markdown_text = converter.convert(
|
markdown_text = converter.convert(
|
||||||
|
|||||||
80
skills/gitea/SKILL.md
Normal file
80
skills/gitea/SKILL.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
name: gitea
|
||||||
|
description: Gitea repository management via REST API. Use when user explicitly mentions "gitea" for: (1) Creating repositories, (2) Listing repositories, (3) Deleting repositories, (4) Pushing code to Gitea. Default server: https://gitea.server10086.icu, default account: admin. Requires GITEA_TOKEN environment variable.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Gitea Repository Management
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- **Server**: `https://gitea.server10086.icu`
|
||||||
|
- **API Base**: `https://gitea.server10086.icu/api/v1`
|
||||||
|
- **Default Account**: `admin`
|
||||||
|
- **Auth Header**: `Authorization: token $GITEA_TOKEN`
|
||||||
|
|
||||||
|
**Always verify GITEA_TOKEN exists before any operation:**
|
||||||
|
```bash
|
||||||
|
if [ -z "$GITEA_TOKEN" ]; then
|
||||||
|
echo "Error: GITEA_TOKEN environment variable not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Repository Operations
|
||||||
|
|
||||||
|
### List Repositories
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all repos for current user
|
||||||
|
curl -s -X GET "https://gitea.server10086.icu/api/v1/user/repos" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://gitea.server10086.icu/api/v1/admin/repos" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "repo-name",
|
||||||
|
"description": "Repository description",
|
||||||
|
"private": false,
|
||||||
|
"auto_init": false
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X DELETE "https://gitea.server10086.icu/api/v1/repos/admin/repo-name" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create and Push Current Directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
REPO_NAME=$(basename $(pwd))
|
||||||
|
|
||||||
|
# Create repo via API
|
||||||
|
curl -X POST "https://gitea.server10086.icu/api/v1/admin/repos" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"name\":\"$REPO_NAME\",\"auto_init\":false}"
|
||||||
|
|
||||||
|
# Initialize and push
|
||||||
|
git init
|
||||||
|
git add .
|
||||||
|
git commit -m "Initial commit"
|
||||||
|
git remote add origin "https://gitea.server10086.icu/admin/$REPO_NAME.git"
|
||||||
|
git push -u origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
| Status | Meaning |
|
||||||
|
|--------|---------|
|
||||||
|
| 401 | Invalid or missing GITEA_TOKEN |
|
||||||
|
| 409 | Repository already exists |
|
||||||
|
| 404 | Repository not found |
|
||||||
|
| 204 | Successful deletion (expected) |
|
||||||
118
skills/minio-upload/SKILL.md
Normal file
118
skills/minio-upload/SKILL.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
---
|
||||||
|
name: minio-upload
|
||||||
|
description: Upload files to Obsidian vaults via MinIO obsidian bucket. Use when user asks to upload files to Obsidian, transfer notes to vaults, sync markdown files to Obsidian, or specify a particular vault destination. The obsidian bucket serves as the sync directory for Obsidian note-taking app.
|
||||||
|
---
|
||||||
|
|
||||||
|
# MinIO Upload
|
||||||
|
|
||||||
|
Upload files to Obsidian vaults through the MinIO obsidian bucket. This bucket serves as the synchronization directory for the Obsidian note-taking application, where each top-level directory represents a separate vault.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
Upload a file to the default `main` vault:
|
||||||
|
```bash
|
||||||
|
.claude/skills/minio-upload/scripts/upload_to_obsidian.sh path/to/file.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Upload to a specific vault:
|
||||||
|
```bash
|
||||||
|
.claude/skills/minio-upload/scripts/upload_to_obsidian.sh path/to/file.md "journal"
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Conflict Handling
|
||||||
|
|
||||||
|
**CRITICAL**: When a file with the same name already exists in the target vault, the script will prompt you to choose an action (unless an option is specified).
|
||||||
|
|
||||||
|
### Interactive Prompt (Default)
|
||||||
|
|
||||||
|
If the file exists and no option is provided, you'll see:
|
||||||
|
```
|
||||||
|
⚠️ File 'filename.md' already exists in vault 'main'
|
||||||
|
Choose an action:
|
||||||
|
[o] Overwrite - Replace existing file
|
||||||
|
[s] Skip - Cancel upload
|
||||||
|
[b] Backup - Backup existing file (.bak) then overwrite
|
||||||
|
[r] Rename - Upload with new name (adds numeric suffix)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command-Line Options
|
||||||
|
|
||||||
|
You can specify the behavior explicitly to skip the prompt:
|
||||||
|
|
||||||
|
- `--force` - Overwrite existing files without prompting
|
||||||
|
- `--skip` - Skip upload if file already exists
|
||||||
|
- `--backup` - Backup existing file (creates `.bak` file) before overwriting
|
||||||
|
- `--rename` - Automatically rename with numeric suffix (e.g., `file-1.md`, `file-2.md`)
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Force overwrite without prompting
|
||||||
|
upload_to_obsidian.sh file.md main --force
|
||||||
|
|
||||||
|
# Skip if file exists
|
||||||
|
upload_to_obsidian.sh file.md journal --skip
|
||||||
|
|
||||||
|
# Backup before overwriting
|
||||||
|
upload_to_obsidian.sh file.md work --backup
|
||||||
|
|
||||||
|
# Auto-rename if conflict
|
||||||
|
upload_to_obsidian.sh file.md personal --rename
|
||||||
|
```
|
||||||
|
|
||||||
|
## Vault Configuration
|
||||||
|
|
||||||
|
**CRITICAL RULE**: When no vault is explicitly specified by the user, files MUST be uploaded to the `main` vault only. Do NOT assume or invent other vault names.
|
||||||
|
|
||||||
|
### Understanding Vault Structure
|
||||||
|
|
||||||
|
The MinIO obsidian bucket contains Obsidian vaults as top-level directories:
|
||||||
|
- **Bucket root**: `minio/obsidian/`
|
||||||
|
- **Vaults**: Each top-level directory is a separate Obsidian vault (e.g., `main/`, `journal/`, `work/`)
|
||||||
|
- **Files uploaded to**: `minio/obsidian/{vault-name}/{filename}`
|
||||||
|
|
||||||
|
### Vault Selection Behavior
|
||||||
|
|
||||||
|
- **No vault specified**: Upload to `main` vault (DEFAULT - use this unless user explicitly requests otherwise)
|
||||||
|
- **Specific vault specified**: Use the exact vault name provided by the user (e.g., `journal`, `work-notes`, `personal`)
|
||||||
|
- **Nested directories**: If a vault name contains slashes, it will be normalized (e.g., `notes/journal` becomes `notes/journal/`)
|
||||||
|
|
||||||
|
### Important Constraints
|
||||||
|
|
||||||
|
1. **Never assume vaults**: If the user doesn't specify a vault, always use `main`
|
||||||
|
2. **User intent only**: Only use non-default vaults when the user explicitly names them
|
||||||
|
3. **Vault validation**: The script automatically normalizes paths and appends the filename
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
- User says "upload file.md" → Upload to `main` vault
|
||||||
|
- User says "upload to my journal vault" → Upload to `journal` vault
|
||||||
|
- User says "upload to work-notes" → Upload to `work-notes` vault
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Verify file exists** - Check that the source file is accessible
|
||||||
|
2. **Determine target vault** - Use specified vault name or default to `main`
|
||||||
|
3. **Check for conflicts** - Detect if file already exists in target vault
|
||||||
|
4. **Resolve conflict** - Prompt user or use specified option to handle conflict
|
||||||
|
5. **Upload with mc** - Execute `mc cp` command to transfer file
|
||||||
|
6. **Confirm success** - Verify upload completed and report target location
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The script will:
|
||||||
|
- Exit with error if file doesn't exist
|
||||||
|
- Exit with error if no file argument provided
|
||||||
|
- Propagate any mc command failures
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
### scripts/upload_to_obsidian.sh
|
||||||
|
|
||||||
|
Bash script that handles the upload process. Validates inputs, constructs the target vault path, and executes the mc copy command.
|
||||||
|
|
||||||
|
**Key features:**
|
||||||
|
- Default to `main` vault when no vault name specified
|
||||||
|
- Support vault names with proper path normalization
|
||||||
|
- Clear error messages for invalid inputs
|
||||||
|
- Success confirmation with source and target paths
|
||||||
198
skills/minio-upload/scripts/upload_to_obsidian.sh
Executable file
198
skills/minio-upload/scripts/upload_to_obsidian.sh
Executable file
@@ -0,0 +1,198 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Upload files to MinIO obsidian bucket
|
||||||
|
# Usage: upload_to_obsidian.sh <file_path> [subpath] [options]
|
||||||
|
#
|
||||||
|
# Arguments:
|
||||||
|
# file_path - Path to the file to upload
|
||||||
|
# subpath - Optional subpath/vault within obsidian bucket (default: main)
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# --force - Overwrite existing files without prompting
|
||||||
|
# --skip - Skip upload if file already exists
|
||||||
|
# --backup - Backup existing file before overwriting (.bak extension)
|
||||||
|
# --rename - Automatically rename if file exists (adds numeric suffix)
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# upload_to_obsidian.sh temp/file.md
|
||||||
|
# upload_to_obsidian.sh temp/file.md journal
|
||||||
|
# upload_to_obsidian.sh temp/file.md main --force
|
||||||
|
# upload_to_obsidian.sh temp/file.md work --skip
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Initialize variables
|
||||||
|
FORCE=false
|
||||||
|
SKIP=false
|
||||||
|
BACKUP=false
|
||||||
|
RENAME=false
|
||||||
|
FILE_PATH=""
|
||||||
|
SUBPATH="main"
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--force)
|
||||||
|
FORCE=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--skip)
|
||||||
|
SKIP=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--backup)
|
||||||
|
BACKUP=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--rename)
|
||||||
|
RENAME=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
echo "Error: Unknown option '$1'" >&2
|
||||||
|
echo "Usage: $0 <file_path> [subpath] [--force|--skip|--backup|--rename]" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [ -z "$FILE_PATH" ]; then
|
||||||
|
FILE_PATH="$1"
|
||||||
|
elif [ "$SUBPATH" = "main" ]; then
|
||||||
|
SUBPATH="$1"
|
||||||
|
else
|
||||||
|
echo "Error: Too many arguments" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Validate mutual exclusion of conflict handling options
|
||||||
|
CONFLICT_OPTS_COUNT=0
|
||||||
|
[ "$FORCE" = true ] && CONFLICT_OPTS_COUNT=$((CONFLICT_OPTS_COUNT + 1))
|
||||||
|
[ "$SKIP" = true ] && CONFLICT_OPTS_COUNT=$((CONFLICT_OPTS_COUNT + 1))
|
||||||
|
[ "$BACKUP" = true ] && CONFLICT_OPTS_COUNT=$((CONFLICT_OPTS_COUNT + 1))
|
||||||
|
[ "$RENAME" = true ] && CONFLICT_OPTS_COUNT=$((CONFLICT_OPTS_COUNT + 1))
|
||||||
|
|
||||||
|
if [ $CONFLICT_OPTS_COUNT -gt 1 ]; then
|
||||||
|
echo "Error: Conflict handling options are mutually exclusive." >&2
|
||||||
|
echo "Use only one of: --force, --skip, --backup, --rename" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check file path argument
|
||||||
|
if [ -z "$FILE_PATH" ]; then
|
||||||
|
echo "Usage: $0 <file_path> [subpath] [--force|--skip|--backup|--rename]" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate file exists
|
||||||
|
if [ ! -f "$FILE_PATH" ]; then
|
||||||
|
echo "Error: File '$FILE_PATH' does not exist" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get filename
|
||||||
|
FILENAME=$(basename "$FILE_PATH")
|
||||||
|
|
||||||
|
# Function to find unique filename by adding numeric suffix
|
||||||
|
# Sets global variables: TARGET_PATH, FILENAME
|
||||||
|
find_unique_filename() {
|
||||||
|
local original_filename="$1"
|
||||||
|
local subpath="$2"
|
||||||
|
local counter=1
|
||||||
|
local base_name="${original_filename%.*}"
|
||||||
|
local ext="${original_filename##*.}"
|
||||||
|
|
||||||
|
# Handle files without extension
|
||||||
|
if [ "$base_name" = "$ext" ]; then
|
||||||
|
base_name="$original_filename"
|
||||||
|
ext=""
|
||||||
|
else
|
||||||
|
ext=".$ext"
|
||||||
|
fi
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
local new_name="${base_name}-${counter}${ext}"
|
||||||
|
local new_target="minio/obsidian/${subpath}/${new_name}"
|
||||||
|
if ! mc stat "$new_target" >/dev/null 2>&1; then
|
||||||
|
TARGET_PATH="$new_target"
|
||||||
|
FILENAME="$new_name"
|
||||||
|
echo "📝 Renamed to: $FILENAME"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
counter=$((counter + 1))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Construct target path
|
||||||
|
TARGET_PATH="minio/obsidian/${SUBPATH}/${FILENAME}"
|
||||||
|
|
||||||
|
# Ensure subpath ends without trailing slash for mc
|
||||||
|
TARGET_PATH=$(echo "$TARGET_PATH" | sed 's://*:/:g')
|
||||||
|
|
||||||
|
# Check if file already exists
|
||||||
|
if mc stat "$TARGET_PATH" >/dev/null 2>&1; then
|
||||||
|
# User explicitly specified --skip
|
||||||
|
if [ "$SKIP" = true ]; then
|
||||||
|
echo "ℹ️ File already exists, skipping upload: $TARGET_PATH"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# User explicitly specified --backup
|
||||||
|
if [ "$BACKUP" = true ]; then
|
||||||
|
BACKUP_PATH="${TARGET_PATH}.bak"
|
||||||
|
echo "📦 Backing up existing file: $TARGET_PATH -> $BACKUP_PATH"
|
||||||
|
mc cp "$TARGET_PATH" "$BACKUP_PATH"
|
||||||
|
# User explicitly specified --rename
|
||||||
|
elif [ "$RENAME" = true ]; then
|
||||||
|
find_unique_filename "$FILENAME" "$SUBPATH"
|
||||||
|
# User explicitly specified --force
|
||||||
|
elif [ "$FORCE" = true ]; then
|
||||||
|
echo "⚠️ Overwriting existing file: $TARGET_PATH"
|
||||||
|
else
|
||||||
|
# No explicit option - prompt user
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ File '$FILENAME' already exists in vault '${SUBPATH}'"
|
||||||
|
echo "Target: $TARGET_PATH"
|
||||||
|
echo ""
|
||||||
|
echo "Choose an action:"
|
||||||
|
echo " [o] Overwrite - Replace existing file"
|
||||||
|
echo " [s] Skip - Cancel upload"
|
||||||
|
echo " [b] Backup - Backup existing file (.bak) then overwrite"
|
||||||
|
echo " [r] Rename - Upload with new name (adds numeric suffix)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
read -p "Your choice [o/s/b/r]: " choice
|
||||||
|
case "$choice" in
|
||||||
|
o|O|overwrite)
|
||||||
|
echo "Overwriting existing file..."
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
s|S|skip)
|
||||||
|
echo "❌ Upload cancelled"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
b|B|backup)
|
||||||
|
BACKUP_PATH="${TARGET_PATH}.bak"
|
||||||
|
echo "📦 Backing up existing file: $TARGET_PATH -> $BACKUP_PATH"
|
||||||
|
mc cp "$TARGET_PATH" "$BACKUP_PATH"
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
r|R|rename)
|
||||||
|
find_unique_filename "$FILENAME" "$SUBPATH"
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Invalid choice. Please enter o, s, b, or r"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Upload file
|
||||||
|
mc cp "$FILE_PATH" "$TARGET_PATH"
|
||||||
|
|
||||||
|
echo "✅ Uploaded: $FILE_PATH -> $TARGET_PATH"
|
||||||
Reference in New Issue
Block a user