# Data Repository API v1

## 1. Overview

Data Repository API v1은 Data Repository의 저장 데이터를 외부 클라이언트가 읽기 전용으로 탐색하고 파일을 다운로드할 수 있도록 제공하는 HTTP API입니다.

공식 Public API 경계:

```text
/api/v1/**
```

현재 v1은 **Read-only**입니다.

| Method | Endpoint | Description |
|---|---|---|
| GET | `/api/v1/directory` | 디렉터리 정보 조회 |
| GET | `/api/v1/directory/contents` | 디렉터리의 하위 디렉터리와 항목 조회 |
| GET | `/api/v1/assets/{id}` | 항목 상세 및 파일 목록 조회 |
| GET | `/api/v1/assets/{id}/files/{file_id}/download` | 파일 다운로드 |

## 2. Base URL

현재 Internal / Tailscale Base URL:

```text
http://100.89.135.42:18432
```

정식 도메인과 HTTPS가 구성되면 Base URL을 새 주소로 교체합니다.

## 3. Versioning

현재 API version:

```text
/api/v1
```

클라이언트는 `/api/v1/**`만 공식 API 계약으로 사용해야 합니다.

## 4. Authentication and Access Control

현재 API v1에는 애플리케이션 레벨의 API Key, Bearer Token, HTTP Basic 등의 별도 인증이 구현되어 있지 않습니다.

현재 접근 제어는 네트워크/Tailscale/Reverse Proxy 등 애플리케이션 외부 계층에 의존합니다.

> 공용 인터넷에 직접 공개하기 전에는 인증 또는 Reverse Proxy 수준의 접근 제어 정책을 추가하는 것을 권장합니다.

## 5. Content Type

JSON 응답:

```http
Content-Type: application/json; charset=utf-8
```

파일 다운로드 endpoint의 최초 응답은 redirect입니다.

## 6. Directory Path Semantics

Directory API의 `path` query parameter는 **Data Root인 `data` 아래의 상대 경로**입니다.

예를 들어 전체 경로가:

```text
data/core/SMS
```

라면 API 요청에는:

```text
core/SMS
```

를 사용합니다.

올바른 예:

```text
?path=core/SMS
?path=개인자료/사진/2026
```

잘못된 예:

```text
?path=data
?path=data/core/SMS
```

`data` prefix를 포함하면 `400 invalid_path`가 반환됩니다.

### Root Directory

Root는 `path`를 생략하거나 빈 문자열로 요청합니다.

```http
GET /api/v1/directory
GET /api/v1/directory?path=
```

현재 root 응답 예:

```json
{
  "directory": {
    "id": 1,
    "name": "data",
    "path": "data",
    "relative_path": "",
    "root": true
  }
}
```

### Unicode Path

한글 등 Unicode path를 지원합니다.

예:

```text
개인자료/사진/2026
```

클라이언트는 query parameter를 정상적으로 URL encoding해야 합니다.

## 7. Timestamps

시간 값은 ISO 8601 문자열이며 timezone offset을 포함합니다.

예:

```text
2026-08-07T11:21:56.997+09:00
```

---

# 8. Directory API

## 8.1 Get Directory

### Request

```http
GET /api/v1/directory
GET /api/v1/directory?path={relative_path}
```

| Parameter | Type | Required | Description |
|---|---|---:|---|
| `path` | string | No | `data` 기준 상대 경로. 생략/빈 값이면 root |

### Example

```bash
curl -sS --get \
  --data-urlencode "path=core/SMS" \
  "http://100.89.135.42:18432/api/v1/directory"
```

### Response — 200 OK

```json
{
  "directory": {
    "id": 3,
    "name": "SMS",
    "path": "data/core/SMS",
    "relative_path": "core/SMS",
    "root": false
  }
}
```

### Directory Object

| Field | Type | Description |
|---|---|---|
| `id` | integer | Directory identifier |
| `name` | string | Directory name |
| `path` | string | 전체 canonical path |
| `relative_path` | string | `data` 아래 상대 경로 |
| `root` | boolean | Data Root 여부 |

## 8.2 Get Directory Contents

디렉터리 자체 정보, 직속 하위 디렉터리, 살아 있는(non-trash) 항목을 함께 반환합니다.

### Request

```http
GET /api/v1/directory/contents
GET /api/v1/directory/contents?path={relative_path}
```

### Example

```bash
curl -sS --get \
  --data-urlencode "path=core/SMS" \
  "http://100.89.135.42:18432/api/v1/directory/contents"
```

### Response — 200 OK

```json
{
  "directory": {
    "id": 3,
    "name": "SMS",
    "path": "data/core/SMS",
    "relative_path": "core/SMS",
    "root": false
  },
  "directories": [],
  "assets": [
    {
      "id": 13,
      "name": "정렬 테스트",
      "category": "테스트",
      "archived": false,
      "file_count": 0,
      "total_bytes": 0,
      "updated_at": "2026-08-07T11:21:56.997+09:00"
    }
  ]
}
```

### Child Directory Object

| Field | Type | Description |
|---|---|---|
| `id` | integer | Directory identifier |
| `name` | string | Directory name |
| `path` | string | 전체 canonical path |
| `relative_path` | string | `data` 아래 상대 경로 |

### Asset Summary Object

| Field | Type | Description |
|---|---|---|
| `id` | integer | Asset identifier |
| `name` | string | Asset name |
| `category` | string | Category name |
| `archived` | boolean | Archive 상태 |
| `file_count` | integer | 살아 있는 file 수 |
| `total_bytes` | integer | 살아 있는 file 총 byte 수 |
| `updated_at` | string | ISO 8601 timestamp |

현재 구현의 ordering:

- `directories`: `name` 오름차순
- `assets`: `updated_at` 내림차순

문자열 정렬은 DB collation의 영향을 받을 수 있습니다.

`assets`에는 `kept` 상태의 항목만 포함됩니다. 휴지통 항목은 노출되지 않습니다.

---

# 9. Asset API

## 9.1 Get Asset

### Request

```http
GET /api/v1/assets/{id}
```

| Parameter | Type | Description |
|---|---|---|
| `id` | integer | Asset identifier |

### Example

```bash
curl -sS \
  "http://100.89.135.42:18432/api/v1/assets/14"
```

### Response — 200 OK

```json
{
  "asset": {
    "id": 14,
    "name": "테스트 자료",
    "archived": false,
    "category": {
      "id": 4,
      "name": "테스트"
    },
    "directory": {
      "id": 3,
      "name": "SMS",
      "path": "data/core/SMS",
      "relative_path": "core/SMS"
    },
    "file_count": 1,
    "total_bytes": 1462848,
    "created_at": "2026-08-06T11:10:00.430+09:00",
    "updated_at": "2026-08-06T11:10:00.430+09:00"
  },
  "files": [
    {
      "id": 5,
      "filename": "ChatGPT Installer.exe",
      "content_type": "application/x-msdownload;format=pe32",
      "byte_size": 1462848,
      "created_at": "2026-08-06T11:10:00.463+09:00"
    }
  ]
}
```

> 위 ID/이름/파일명/크기는 runtime 검증 당시의 예시 데이터이며 고정 계약이 아닙니다.

### Asset Object

| Field | Type | Description |
|---|---|---|
| `id` | integer | Asset identifier |
| `name` | string | Asset name |
| `archived` | boolean | Archive 상태 |
| `category.id` | integer | Category identifier |
| `category.name` | string | Category name |
| `directory.id` | integer | Directory identifier |
| `directory.name` | string | Directory name |
| `directory.path` | string | 전체 canonical path |
| `directory.relative_path` | string | 상대 path |
| `file_count` | integer | 살아 있는 file 수 |
| `total_bytes` | integer | 살아 있는 file 총 byte 수 |
| `created_at` | string | ISO 8601 timestamp |
| `updated_at` | string | ISO 8601 timestamp |

### File Object

| Field | Type | Description |
|---|---|---|
| `id` | integer | 공개 API의 file identifier (`AssetFile` ID) |
| `filename` | string | 원본 file name |
| `content_type` | string | MIME/content type |
| `byte_size` | integer | File size in bytes |
| `created_at` | string | ISO 8601 timestamp |

다운로드 endpoint의 `{file_id}`에는 `files[].id`를 사용합니다.

클라이언트는 Active Storage Blob ID나 storage key를 추측하거나 사용해서는 안 됩니다.

---

# 10. File Download API

## 10.1 Download File

### Request

```http
GET /api/v1/assets/{id}/files/{file_id}/download
```

| Parameter | Type | Description |
|---|---|---|
| `id` | integer | Asset identifier |
| `file_id` | integer | Asset 상세의 `files[].id` |

### Example

```bash
curl -fL \
  "http://100.89.135.42:18432/api/v1/assets/14/files/5/download" \
  -o downloaded-file
```

### Redirect Contract

최초 응답은 production runtime 기준:

```http
HTTP/1.1 302 Found
Location: http://.../rails/active_storage/...
```

클라이언트는 redirect를 따라가야 합니다. `curl`에서는 `-L`을 사용합니다.

Runtime 검증에서는 예상 `1462848` bytes 파일이 최종 다운로드 후 실제로 `1462848` bytes와 일치했습니다.

### Internal URL Warning

다음은 내부 구현 경로입니다.

```text
/rails/active_storage/**
```

클라이언트는 redirect URL을 저장하거나 조립해서는 안 됩니다.

항상 공식 endpoint:

```text
/api/v1/assets/{id}/files/{file_id}/download
```

에서 다운로드를 시작합니다.

---

# 11. Error Contract

공통 JSON error envelope:

```json
{
  "error": {
    "code": "...",
    "message": "..."
  }
}
```

## 11.1 Invalid Path — 400 Bad Request

예:

```http
GET /api/v1/directory?path=data
```

응답:

```json
{
  "error": {
    "code": "invalid_path",
    "message": "data는 생략하고 data 아래의 상대 경로만 입력해주세요"
  }
}
```

클라이언트는 사람이 읽는 `message`보다 `error.code`를 분기 기준으로 사용하는 것이 권장됩니다.

## 11.2 Not Found — 404 Not Found

존재하지 않는 정상 형식의 Directory, Asset, File은 동일한 오류를 반환합니다.

```json
{
  "error": {
    "code": "not_found",
    "message": "요청한 리소스를 찾을 수 없습니다."
  }
}
```

Runtime에서 다음을 모두 검증했습니다.

- 존재하지 않는 Directory
- 존재하지 않는 Asset
- 존재하는 Asset 아래의 존재하지 않는 File

---

# 12. HTTP Status Summary

| Status | Meaning |
|---:|---|
| `200 OK` | Directory / Contents / Asset 조회 성공 |
| `302 Found` | File download endpoint redirect |
| `400 Bad Request` | 잘못된 directory path |
| `404 Not Found` | Directory / Asset / File 없음 |

그 외 서버 오류는 일반 HTTP 오류로 취급하며 현재 v1의 안정된 application error contract로 별도 정의되어 있지 않습니다.

---

# 13. Recommended Client Flow

```text
1. GET /api/v1/directory/contents
        ↓
2. directories[].relative_path 로 하위 탐색
        ↓
3. assets[].id 확보
        ↓
4. GET /api/v1/assets/{id}
        ↓
5. files[].id 확보
        ↓
6. GET /api/v1/assets/{id}/files/{file_id}/download
        ↓
7. HTTP redirect를 따라 file 수신
```

Directory 탐색에는 응답의 `relative_path`를 사용하는 것이 가장 안전합니다.

---

# 14. curl Quick Start

```bash
BASE_URL="http://100.89.135.42:18432"
```

Root contents:

```bash
curl -sS "$BASE_URL/api/v1/directory/contents"
```

Nested contents:

```bash
curl -sS --get \
  --data-urlencode "path=core/SMS" \
  "$BASE_URL/api/v1/directory/contents"
```

Asset:

```bash
curl -sS "$BASE_URL/api/v1/assets/14"
```

Download:

```bash
curl -fL \
  "$BASE_URL/api/v1/assets/14/files/5/download" \
  -o output.bin
```

---

# 15. Public API Boundary

## Supported Public Contract

```text
/api/v1/**
```

현재 공식 endpoint:

```text
GET /api/v1/directory
GET /api/v1/directory/contents
GET /api/v1/assets/{id}
GET /api/v1/assets/{id}/files/{file_id}/download
```

## Not Public API

```text
/directories/**
/assets/**
/categories/**
/rails/**
```

특히:

```text
/rails/active_storage/**
```

는 Rails/Active Storage 내부 구현입니다.

클라이언트는 내부 route, signed blob URL, storage key, Rails controller 이름 등에 의존해서는 안 됩니다.

---

# 16. Read-only Contract

API v1에는 mutation endpoint가 없습니다.

지원하지 않는 Public API 기능:

```text
POST
PATCH
PUT
DELETE
upload
restore
purge
trash mutation
```

웹 UI에 mutation route가 존재하더라도 API v1 Public Contract가 아닙니다.

---

# 17. Compatibility Guidance

클라이언트 권장 원칙:

1. `/api/v1`만 공식 API namespace로 사용합니다.
2. 미래에 새 JSON field가 추가될 수 있으므로 알 수 없는 field를 허용합니다.
3. breaking change는 새로운 API version에서 처리하는 것을 원칙으로 합니다.
4. `id`는 opaque identifier처럼 취급하고 의미나 연속성을 추론하지 않습니다.
5. File download redirect URL을 저장하지 않습니다.
6. Directory 탐색에는 `relative_path`를 사용합니다.
7. Timestamp는 timezone-aware ISO 8601로 파싱합니다.
8. Error handling은 `message`보다 `error.code`를 우선합니다.

---

# 18. Runtime Validation Record

이 문서는 Rails route/controller 구현과 Synology NAS production runtime의 실제 HTTP 호출을 함께 검증하여 작성되었습니다.

검증 완료:

```text
GET Directory root                         200
GET Directory empty path                   200
GET Directory relative path                200
GET Directory Unicode path                 200
GET Directory Contents root                200
GET Directory Contents nested              200
GET Asset                                  200
GET File Download                          302
Follow File Download                       success
Invalid "data" prefix                      400 invalid_path
Nonexistent Directory                      404 not_found
Nonexistent Asset                          404 not_found
Nonexistent File                           404 not_found
JSON Content-Type                          verified
ISO 8601 timezone-aware timestamps         verified
```

---

# 19. Current Deployment Note

현재 검증 환경:

```text
Environment: Production
Host: Synology NAS
Internal access: Tailscale
API Base URL: http://100.89.135.42:18432
```

정식 도메인 및 HTTPS가 적용되면 Base URL을 갱신해야 합니다.

---

# 20. Security Note

현재 API에는 별도 application-level authentication이 없습니다.

현재 v1은 신뢰된 내부 네트워크/Tailscale 환경에서 사용하는 것을 전제로 합니다.

공용 인터넷 공개 전에는 다음 중 적절한 보호 계층을 설계해야 합니다.

- API authentication
- Reverse Proxy authentication/access policy
- IP/network allow-list
- TLS/HTTPS

인증 방식이 추가되면 본 문서의 Authentication section과 request example도 함께 갱신해야 합니다.
