Tiny Tube는 교육용 동영상, 발표 자료, 녹화 회의를 자체 서버에서 공유하는 오픈소스 프로그램이다. Go 실행 파일 하나로 구동되며 웹 인터페이스와 SQLite 메타데이터 저장소를 포함한다. 동영상 파일은 디스크에 저장하고, Windows·Linux·macOS에서 사용할 수 있다.
검색 가능한 라이브러리에서 카테고리별 탐색과 정렬, 재생 속도 조절, 원본 파일 다운로드를 지원한다. 인터페이스 언어는 영어와 그리스어다. FFmpeg를 설치하면 HLS 변환과 포스터 이미지 생성을 사용할 수 있으며, FFmpeg가 없어도 브라우저가 코덱을 지원하는 원본 파일은 재생할 수 있다. 로그인은 로컬 계정이나 LDAP를 지원한다. 편집자는 업로드와 수정, 관리자는 여기에 더해 삭제와 사용자 관리를 할 수 있다.
- 릴리스 다운로드 · 설치 · 설정 · HTTP API · 라이선스
- 동영상 디렉터리마다 서버 프로세스를 하나만 실행해야 한다. 여러 인스턴스가 같은 저장소를 공유하는 방식은 지원하지 않는다.
- HLS는 현재 높이 720픽셀인 단일 화질만 제공한다. 적응형 화질, 백업, 저장 공간 할당량 기능은 내장돼 있지 않다. 배포 세부 사항은 검토 문서의 남은 제한을 참고할 수 있다.
설치와 첫 실행
운영체제에 맞는 아카이브를 GitHub Releases에서 내려받는다. Windows는 .zip, Linux와 macOS는 .tar.gz를 사용한다. Intel·AMD 시스템은 amd64, ARM 시스템은 arm64를 선택하며 Apple Silicon도 arm64에 해당한다. 압축을 푼 뒤 해당 폴더에서 터미널을 연다. 배포 바이너리는 서명되지 않았으므로 Windows나 macOS에서 보안 경고가 나타날 수 있다.
릴리스에는 서버, 관리 도구 tiny-tube-cli, 설정 예시, 문서, VERSION.txt가 들어 있다. Go나 별도 데이터베이스 서버는 필요하지 않다. SQLite와 웹 인터페이스가 포함돼 있다. 원본 파일과 생성 미디어를 보관할 수 있도록 쓰기 가능한 동영상 디렉터리와 충분한 공간을 준비해야 한다.
다운로드 검증은 릴리스의 SHA256SUMS에서 같은 파일명의 해시와 비교한다. v1.0.3 amd64 아카이브를 확인하는 명령은 다음과 같다.
# Windows PowerShell
Get-FileHash .\\tiny-tube-v1.0.3-windows-amd64.zip -Algorithm SHA256
# Linux
sha256sum tiny-tube-v1.0.3-linux-amd64.tar.gz
# macOS (use darwin-arm64 for Apple Silicon)
shasum -a 256 tiny-tube-v1.0.3-darwin-amd64.tar.gz
실제로 사용할 버전과 플랫폼에 맞는 파일명을 지정해야 한다.
처음 설치할 때 압축을 푼 폴더에서 .env.example을 .env로 복사한다.
# Windows PowerShell
Copy-Item .env.example .env
# Linux / macOS
cp .env.example .env
텍스트 편집기로 .env를 열어 ADMIN_PASS에 강한 비밀번호를 설정한다. 기본 관리자 이름은 admin이며, 원한다면 ADMIN_USER도 바꿀 수 있다. 비밀번호는 비워 두거나 changeme로 설정할 수 없다. 자격 증명이 들어갈 수 있으므로 .env를 비공개로 유지해야 한다. 업그레이드할 때 기존 .env를 덮어쓰지 않는다.
같은 폴더에서 서버를 실행한다.
# Windows PowerShell
.\\tiny-tube.exe
# Linux / macOS
./tiny-tube
같은 컴퓨터의 브라우저에서 http://localhost:8081에 접속해 관리자 계정으로 로그인한다. 동영상은 ‘Upload video’에서 추가하고, 사용자는 ‘Administration’에서 관리한다. 서버를 종료하려면 터미널에서 Ctrl+C를 누른다.
사용자 테이블이 비어 있을 때 처음 실행하면 관리자 계정이 생성된다. 이후 사용자와 비밀번호는 /admin에서 관리한다. 계정이 만들어진 뒤 ADMIN_USER나 ADMIN_PASS를 바꿔도 기존 계정은 바뀌지 않는다. 라이브러리는 처음에 비어 있으며 기본적으로 현재 작업 디렉터리 아래 videos/에 동영상과 SQLite 데이터베이스를 저장한다.
HLS 변환과 포스터 자동 생성을 사용하려면 libx264 및 AAC 인코더가 포함된 FFmpeg를 별도로 설치하고 PATH에 추가하거나 FFMPEG_BIN을 설정한다. FFmpeg가 없으면 브라우저가 파일 형식과 코덱을 지원하는 경우 원본 파일을 재생한다.
기본 주소는 로컬 연결만 허용한다. 네트워크에서 접속하려면 주소와 HTTPS를 설정해야 한다. 자세한 내용은 설정과 Nginx 예시를 참고한다. Tiny Tube는 백업을 자동으로 만들지 않으므로 동영상과 데이터베이스의 백업을 계획해야 한다. 보안 문서와 남은 제한도 확인할 수 있다.
Linux에서 장기간 실행하려면 사용할 작업 디렉터리, 절대 경로인 VIDEO_DIR, 자격 증명을 지정해 서비스로 설정한다. 변환을 사용한다면 서비스가 FFmpeg를 찾을 수 있어야 한다. systemd 유닛 파일은 제공되지 않는다.
웹 인터페이스 없이 운영자 역할을 수정할 때는 릴리스에 포함된 tiny-tube-cli의 users list와 users set-role --username U --role admin|editor|viewer를 사용할 수 있다. 서버와 같은 VIDEO_DIR, STORE, DB_PATH 설정을 적용해야 한다. 활성 관리자가 마지막 한 명뿐인 경우 CLI는 그 계정의 관리자 권한 강등을 거부한다.
비공개 목록과 동영상 접근
공개 동영상은 누구나 볼 수 있다. 민감한 자료에는 관리자가 /admin/lists에서 비공개 목록을 만들 수 있으며, 목록마다 길고 무작위인 키가 부여된다. 업로드와 수정 화면에서 공개 또는 비공개 목록 하나를 선택한다. 기본값은 공개이며, 기존 동영상은 목록으로 옮기지 않는 한 계속 공개 상태다.
목록 링크 /view_list?key=…를 가진 사람은 해당 목록의 동영상만 볼 수 있다. 키가 없으면 그 목록의 동영상은 라이브러리, 검색, 카테고리별 개수, API 목록에서 제외된다. 잠금 해제 쿠키나 편집자·관리자 권한이 없으면 동영상 시청 페이지, 메타데이터, 원본, HLS 조각, 포스터에 직접 요청해도 404를 반환한다. 키를 처음 열면 잠금 해제 쿠키가 설정돼 이후 페이지, 포스터, HLS 조각을 볼 때마다 키를 다시 입력하지 않아도 된다. 편집자와 관리자는 관리 목적으로 키 없이 모든 동영상에 접근할 수 있다.
목록 링크는 소지자에게 접근 권한을 주는 자격 증명이다. 링크를 전달받은 누구나 접근할 수 있으므로 안전한 경로로 공유해야 한다. 키를 교체하면 기존 쿠키를 포함한 모든 사본의 접근이 한 번에 취소된다. 동영상이 남아 있는 목록은 삭제할 수 없어 동영상이 조용히 공개 상태로 바뀌지 않는다. Nginx 예시는 모든 경로를 Go 서버로 전달하므로 프록시 뒤에서도 같은 검사가 적용된다.
메타데이터가 없거나 읽을 수 없다고 해서 미디어 접근을 허용하지 않는다. 메타데이터 없는 고아 파일은 404를 반환하며, 공유하려면 먼저 메타데이터를 복구해야 한다. 공개 범위 변경은 미디어 접근 검사와 함께 동기화된다. 목록 선택기 로드에 실패하면 업로드·수정 양식이 차단된다. API 수정 요청에서 access_list를 생략하면 기존 공개 범위를 유지하고, 공개로 바꾸려면 빈 값을 명시해야 한다. ‘Copy Link’는 유효한 목록 키가 있을 때만 비공개 동영상 URL을 제공한다. 키가 없으면 원래의 비공개 목록 링크가 필요하다는 안내를 표시한다.
스크립트는 /api/videos, /api/video, /watch, /videos/…에 ?key=…를 전달하거나 링크 방문 때 발급된 잠금 해제 쿠키를 재사용할 수 있다.
설정과 보안
서버는 작업 디렉터리의 .env를 읽는다. 명시적인 환경 변수는 빈 값도 포함해 .env보다 우선한다. 값은 따옴표로 감쌀 수 있지만 변수 확장과 줄 안 주석은 지원하지 않는다. .env는 Git에서 무시된다. 선택 항목의 빈 값은 기본값으로 처리된다.
| 변수 | 기본값 | 동작 |
|---|---|---|
VIDEO_DIR | videos | 동영상 저장 위치. 상대 경로는 프로세스 작업 디렉터리를 기준으로 한다. |
ADDR | 설정 안 됨 | 전체 host:port 바인딩 주소. 설정하면 HOST와 PORT보다 우선한다. 기본 유효 주소는 127.0.0.1:8081이다. |
HOST | 127.0.0.1 | ADDR가 없을 때 바인딩 호스트. IPv4 네트워크 접속에는 0.0.0.0을 명시한다. |
PORT | 8081 | ADDR가 없을 때 HTTP 포트. PORT=8080도 계속 지원한다. |
STORE | sqlite | 메타데이터 저장 방식: sqlite 또는 이전 방식인 fs. 동영상 파일은 두 방식 모두 디스크에 남는다. |
DB_PATH | <VIDEO_DIR>/tiny-tube.db | WAL 모드 SQLite 메타데이터 데이터베이스. 상대 경로는 작업 디렉터리를 기준으로 한다. |
ADMIN_USER | admin | 사용자가 없을 때 첫 부팅에서 생성할 관리자. 이후에는 무시한다. |
ADMIN_PASS | 첫 부팅 때 필수 | 첫 관리자 비밀번호. 사용자 테이블이 비어 있을 때 설정되지 않았거나 빈 값 또는 changeme이면 시작을 거부한다. |
AUTH_MODE | local | 로그인 방식: local, ldap, both. both에서는 기존 로컬 계정은 로컬 비밀번호를 사용하고, 나머지 계정은 LDAP를 사용한다. LDAP는 같은 이름의 로컬 계정·역할·키를 가져오지 않는다. |
COOKIE_SECURE | false | HTTPS 뒤에서 true로 설정하면 세션 쿠키에 Secure 플래그를 붙인다. |
LDAP_ADDR | ldap://localhost:389 | 디렉터리 주소(ldap:// 또는 ldaps://). |
LDAP_BASE_DN | dc=example,dc=com | 하위 트리 검색의 대체 검색 기반 및 직접 바인딩 기본값. |
LDAP_BIND_DN | 설정 안 됨 | 하위 트리 검색용 서비스 계정. 설정하지 않으면 사용자가 직접 바인딩한다. |
LDAP_BIND_PASSWORD | 설정 안 됨 | 서비스 계정 비밀번호. |
LDAP_SEARCH_BASES | 설정 안 됨 | 하위 트리 검색 기반을 세미콜론(;)으로 구분한 목록. |
LDAP_USER_FILTER | (uid=%s) | 이스케이프한 사용자 이름을 %s에 넣는 검색 필터. |
LDAP_USER_DN | uid=%s,<base> | 사용자 이름을 넣는 직접 바인딩 DN 템플릿. |
LDAP_ATTR_EMAIL | mail | 사용자 레코드에 매핑할 디렉터리 속성. |
LDAP_ATTR_FIRST_NAME | givenName | 표시 이름에 사용할 디렉터리 속성. |
LDAP_ATTR_LAST_NAME | sn | 표시 이름에 사용할 디렉터리 속성. |
LDAP_ALWAYS_UPDATE | true | 로그인할 때마다 디렉터리에서 LDAP 사용자 레코드를 갱신한다. |
MAX_UPLOAD_BYTES | 2147483648 | 양식 필드와 오버헤드를 포함한 전체 멀티파트 요청의 최대 크기. 2 GiB를 넘으면 HTTP 413을 반환한다. |
TRANSCODE_WORKERS | 1 | 동시 FFmpeg 프로세스 최대 개수. 1~16이며 대기 작업은 디스크에 남는다. |
AUTH_REQUESTS_PER_MINUTE | 60 | 인증 전에 출발지 IP별로 검사하는 보호 경로 요청 한도. 1~3600이며 초과하면 Retry-After와 함께 429를 반환한다. |
UPLOAD_TIMEOUT_SECONDS | 1800 | 인증된 멀티파트 요청을 읽는 전체 제한 시간. 1~86400초. 중단된 업로드는 408을 반환하고 정리한다. |
TRANSCODE_TIMEOUT_SECONDS | 1800 | HLS·포스터 변환 전체 제한 시간. 1~86400초. 시간이 지나면 원본을 보존하고 작업 상태를 failed로 바꾼다. |
FFMPEG_BIN | 설정 안 됨 | 명시적 실행 파일 경로나 이름이며 PATH보다 우선한다. 잘못된 값을 지정하면 시작에 실패한다. 지정하지 않으면 PATH에서 FFmpeg를 찾는다. |
LAN에서 직접 접속하려면 HOST를 명시하거나 ADDR=0.0.0.0:8081로 설정하고 방화벽도 구성한다. 네트워크 사용 시 세션 쿠키에는 HTTPS가 필요하다. Go 서버 자체는 HTTP를 제공한다. 동영상 파일과 공개 메타데이터는 공개되어 있다. 로그인 세션은 활동이 있으면 만료 시점이 연장되는 방식으로 24시간 유지되며, 만료 시각과 브라우저 쿠키는 활동 중 최대 5분에 한 번 갱신된다. 갱신은 살아 있는 세션만 바꾸며 취소된 토큰을 되살리지 않는다. 비밀번호 초기화, 계정 비활성화, 사용자 삭제는 세션을 취소한다. 잠금 해제 쿠키는 프로세스별 서명이 붙은 세션 쿠키다. 브라우저 세션이 끝나거나 서버가 재시작되면 만료되며, 목록 링크를 다시 열면 재발급된다.
이전 빌드가 로컬 사용자 이름을 LDAP에 자동으로 연결했다면 기존 LDAP 역할 부여를 검토해야 한다. 새 로그인 경로는 앞으로의 자동 연결을 막지만 기존 계정과 역할은 유지하며, 과거 인증 출처를 복원하지 않는다. 브라우저에서 발생한 변경 요청은 이 사이트에서 시작해야 한다. 브라우저 출처가 다른 업로드·수정·삭제 요청은 JSON 클라이언트를 포함해 403을 반환한다. 브라우저 출처 헤더가 없는 명령줄 클라이언트는 계속 사용할 수 있다. Accept나 format=json을 바꿔도 인증, 요청 제한, 출처 검사를 우회할 수 없다. CSP는 인라인 스크립트와 인터페이스의 프레임 삽입을 차단하며 MIME 스니핑도 비활성화한다.
인증 요청 제한은 실제 소켓 출발지 주소를 사용하며 전달 헤더는 무시한다. Nginx 뒤에서는 클라이언트가 프록시 IP의 한도 버킷을 공유하므로 Go의 전체 제한을 조정하고 신뢰하는 경계에서 클라이언트별 제한을 적용해야 한다. 공개 보기와 API 읽기는 인증 제한 시간 중에도 가능하다. 제한기 메모리 사용량은 상한이 있다. 헤더 제한 시간은 10초, 헤더 크기 한도는 64 KiB다. 일반 요청 본문 읽기는 30초로 제한하고 인증된 업로드에는 별도 설정을 적용한다. 응답 쓰기 시간 제한은 max(30 minutes, UPLOAD_TIMEOUT_SECONDS - 60 seconds)이므로 매우 느린 원본 다운로드에는 더 큰 설정이 필요할 수 있다.
화면과 업로드·재생
모든 페이지에서 영어와 그리스어를 지원하며, 관리자 화면과 업로드 검증 메시지, 플레이어 안내도 포함한다. 머리글의 언어 선택기는 선택을 브라우저 쿠키에 1년간 저장하며 JavaScript 없이도 작동한다. 선택을 저장하기 전에는 브라우저의 Accept-Language 설정에서 지원 언어를 고르고, 지원되지 않는 언어는 영어로 대체한다. 언어를 바꿔도 현재 페이지, 검색·필터·페이지 이동 매개변수, 비공개 목록 키가 유지된다. 전환 과정에서 페이지가 다시 로드되므로 수정 사항을 저장하거나 업로드를 끝낸 뒤 바꿔야 한다.
날짜, 파일 크기, 조회 수와 동영상 수는 선택한 언어에 맞춰 표시한다. 제목, 설명, 카테고리, 목록 이름 등 사용자가 입력한 내용은 원래 텍스트를 유지한다. JSON API 필드, 상태값, 오류, 서버 로그, CLI 출력은 자동화 호환성을 위해 기존 형식과 언어를 유지한다. 영어 원본 카탈로그는 web/locales/en.json, 그리스어 번역은 같은 안정적인 메시지 ID(예: upload.title)를 사용하는 el.json에 있다. 서버는 이름 있는 변수와 CLDR 복수형 규칙에 go-i18n을, 언어 매칭과 숫자 형식에 golang.org/x/text를 사용한다. 브라우저 메시지는 같은 카탈로그와 Intl.PluralRules, Intl.NumberFormat을 사용하며 CSP 인라인 예외는 필요하지 않다. 번역이 빠진 항목은 영어로 대체한다. 새 언어는 표준 go-i18n JSON 카탈로그와 language.name 항목을 추가한 뒤 다시 빌드하면 된다. 언어 검색, 쿠키, 협상, 선택기에 자동으로 포함되므로 Go·HTML·JS의 언어 목록을 수정할 필요가 없다. 예시, 복수형, 검증 방법은 번역 안내를 참고한다. 프런트엔드 패키지는 필요하지 않다.
기본 화면은 밝은 모드다. 테마 버튼으로 밝은 화면과 어두운 화면을 바꿀 수 있으며 선택은 브라우저 로컬 저장소에 기억된다. 운영체제 테마를 자동으로 따르지는 않는다. 라이브러리, 시청, 업로드 페이지는 모바일·태블릿·데스크톱 화면에 맞게 배치된다. 카테고리와 텍스트 검색을 함께 적용할 수 있다. 라이브러리와 비공개 목록은 기본적으로 페이지당 동영상 24개를 보여주며, 이전·다음 이동에도 검색, 카테고리, 정렬이 유지된다. 긴 공개 범위 표시도 좁은 화면에 맞춘다. 관리자 삭제 확인은 모든 행에 적용되며 삭제 대상이 동영상·사용자·목록 중 무엇인지 표시한다.
업로드 화면은 파일 선택이나 끌어다 놓기를 지원하고 선택한 파일을 표시한다. 업로드 진행률과 오류를 보여주며 양식 내용을 버리지 않는다. JavaScript가 없어도 기본 멀티파트 양식으로 업로드할 수 있다. 시청 페이지에서는 링크 복사와 원본 파일 접근을 제공한다. 화면은 업무용으로 절제된 색과 작업 중심 문구를 사용한다. 카드에는 추출한 포스터가 있으면 표시하고, 없으면 중립적인 대체 이미지를 보여준다. 플레이어 속도는 0.5×부터 2×까지 제공하며 소리를 켠 채 재생을 시도한다. 브라우저가 재생을 막으면 시청자가 재생 버튼을 눌러야 한다. 동영상은 음소거하지 않는다.
시청 페이지는 /edit?id=<id> 수정 링크와 확인 대화상자가 있는 삭제 기능을 제공한다. GET /edit와 POST /api/edit에는 편집자 또는 관리자 로그인이 필요하고, POST /api/delete와 /admin에는 관리자 로그인이 필요하다. 시청 페이지에는 해당 역할이 있는 사용자에게만 관리 기능이 표시된다. 수정은 제목·설명·카테고리·공개 범위를 바꾸며 재생 데이터는 보존한다. 삭제하면 진행 중인 변환을 취소하고 종료될 때까지 기다린 뒤 원본, HLS, 포스터, 메타데이터를 제거한다. 실행 취소 기능은 없다.
권한이 있는 시청 페이지를 불러오면 조회 수를 한 번 기록한다. 같은 시청자 주소와 동영상 조합은 24시간에 한 번만 집계해 새로고침으로 수가 부풀지 않도록 한다. 거부된 요청은 집계하지 않는다. 라이브러리 카드와 시청 페이지에 총 조회 수를 표시한다. 라이브러리와 비공개 목록은 기본 최신순 외에 인기순과 제목순을 제공한다. 제목 정렬은 대소문자를 구분하지 않고 숫자를 자연스럽게 정렬해 Lesson 2를 Lesson 10보다 먼저 둔다. 라이브러리와 API는 sort=views와 sort=title을 받으며 기본 정렬은 최신순이다. 페이지 이동과 필터링에서도 정렬 선택을 유지한다.
인증된 POST /api/upload는 멀티파트 필드 title(필수), desc, category, file을 받는다. 원본은 VIDEO_DIR/<id>/에 저장한다. 인식하는 확장자는 .mp4, .mov, .mkv, .webm이다. 지원하지 않는 확장자나 컨테이너 서명과 맞지 않는 파일은 HTTP 415를 반환한다. 검사는 기본 헤더 확인이며 전체 코덱·파일 검증은 아니다. 제목은 512바이트, 설명은 8,192바이트, 카테고리는 256바이트로 제한한다. 업로드마다 고유 디렉터리를 예약하며 원본이나 메타데이터 저장에 실패하면 미완성 업로드를 제거한다.
FFmpeg를 사용할 수 있으면 메타데이터 상태를 queued로 시작한다. 제한된 작업자가 상태를 processing으로 바꾸고 스테이징 디렉터리에 HLS를 생성한다. FFmpeg가 성공하고 완전한 재생 목록을 만들었을 때만 결과를 공개한다. JPEG 포스터는 1초 지점에서 추출하고 실패하면 첫 프레임을 사용한다. 포스터 생성 실패는 변환 실패로 처리하지 않는다. 변환이 끝나면 상태는 ready가 된다. FFmpeg가 없으면 원본 파일 재생 상태로 즉시 ready가 된다.
변환 후 시청 페이지를 새로고침하면 HLS를 사용할 수 있다. Safari는 네이티브 HLS를 사용하고, 지원되는 다른 브라우저는 hls.js를 쓴다. HLS를 지원하지 않거나 재생에 실패하면 원본 파일로 대체한다. 포함된 hls.js는 1.6.19이며 web/static/hls.min.js에 들어 있다. 라이선스는 Apache-2.0이고 hls.js.LICENSE.txt에 안내돼 있다. 브라우저에서 외부에 접속할 필요는 없다. 변환 실패 상태는 failed가 되고 오류 메시지를 표시하지만 원본은 보존한다. 대기 중이거나 중단된 작업은 FFmpeg를 사용할 수 있으면 시작할 때 재개하고, 사용할 수 없으면 실패 처리한다. 정상 종료 때 진행 중인 변환은 취소 후 대기열에 다시 넣는다. 실패한 작업은 자동으로 재시도하지 않는다.
HTTP API와 저장 방식
GET /api/videos는 페이지 단위로 결과를 반환한다. page는 1부터 시작하고 page_size 기본값은 50, 최대값은 100이다. HTML 화면의 기본값은 24다. 응답에는 전체 표시 라이브러리 크기인 total, 검색 조건과 일치하는 표시 동영상 수인 filtered, 현재 페이지 항목 수인 count, page, page_size, pages, items가 포함된다. 한 번의 요청으로 모든 동영상이 온다고 가정하는 클라이언트는 페이지를 순회해야 한다. 범위를 넘는 양의 페이지 번호는 마지막 페이지로 바뀌며, 잘못되거나 허용치를 초과한 매개변수는 400을 반환한다. 권한 검사는 개수, 카테고리 합계, 페이지 분할보다 먼저 적용한다. SQLite는 쿼리에서 필터와 페이지 처리를 하며 그리스어 검색과 정확한 날짜 정렬을 유지한다. 이전 FS 방식은 JSON 레코드를 훑는다. 준비 상태와 변환 확인은 가벼운 SQLite 쿼리를 사용한다. 조회 중복 방지 메모리는 8,192개 항목으로 제한되며, 사용량이 많아 항목이 밀려나면 조회 수가 추가로 집계될 수 있다.
프로그램에서 로그인하려면 POST /api/login에 {"username", "password"}를 보내고 브라우저처럼 세션 쿠키를 재사용한다. 쿠키를 관리하지 않을 스크립트라면 관리자가 /admin의 API 키 항목이나 cli.bat users api-key --username U --name N으로 사용자별 장기 API 키를 만들 수 있다. 클라이언트는 Authorization: Bearer tt-… 헤더로 키를 보낸다. 키는 사용자 역할을 따르고 만료되지 않는다. /admin 또는 users api-key-revoke --prefix P로 취소할 수 있다. 저장하는 값은 SHA-256뿐이며 평문은 생성 시 한 번만 표시한다.
업로드는 계속 multipart/form-data로 보내며 응답 형식만 선택할 수 있다. Accept: application/json이나 ?format=json을 보내면 브라우저의 303 리디렉션 대신 JSON을 받는다. 오류 응답은 같은 HTTP 상태 코드와 {"error": "..."} 본문을 사용한다. 익명 JSON 클라이언트는 401, 로그인했지만 역할이 맞지 않는 사용자는 403을 받는다. 읽기는 라이브러리와 마찬가지로 공개지만 비공개 목록 동영상에는 예외가 적용된다. 쓰기에는 해당 역할이 필요하다.
| 메서드 | 경로 | 인증 | 설명 |
|---|---|---|---|
POST | /language | 없음 | language=<등록된 locale>(처음에는 en 또는 el)와 선택 항목 next를 받는다. 브라우저 언어를 저장하고 사이트 내부 경로로 되돌아간다. 같은 사이트 출처 검사를 적용한다. |
POST | /api/login | 없음 | JSON {"username", "password"}를 받는다. 세션 쿠키를 설정하고 {"username", "role"}을 반환한다. |
POST | /logout | 세션 | 세션 쿠키를 취소한다. |
POST | /api/upload | 편집자 | title(필수), desc, category, file, access_list(공개는 빈 값, 비공개는 목록 ID)를 받는다. JSON 클라이언트에는 id, title, status, original, access_list, watch_url, video_url, api_url과 201을 반환한다. 브라우저는 /watch?id=로 303 이동한다. |
GET | /view_list?key=… | 없음 | 비공개 목록 하나의 동영상만 보여주는 HTML 페이지이며 잠금 해제 쿠키를 발급한다. 키가 없으면 쿠키로 이미 잠금 해제한 목록을 보여준다. 알 수 없는 키는 404를 반환한다. |
GET | /api/videos?q=&category=&page=1&page_size=50 | 없음 | 라이브러리와 같은 필터를 적용해 total, filtered, count, page, page_size, pages, q, category, sort, items를 JSON으로 반환한다. ?key=…, 잠금 해제 쿠키, 편집자·관리자 인증이 없으면 비공개 동영상은 제외한다. total은 표시 가능한 전체 수, filtered는 조건에 맞는 수, count는 현재 페이지 수다. sort=views는 조회 수순, sort=title은 제목순이며 기본값은 최신순이다. |
GET | /api/video?id=<id> | 없음 | 상태(queued/processing/ready/failed), access_list, views를 포함한 동영상 메타데이터를 반환한다. 없거나 접근할 수 없는 비공개 동영상은 404다. |
POST | /api/edit | 편집자 | id, title, desc, category, access_list를 받는다. JSON 클라이언트에는 id, title, watch_url, api_url과 200을 반환한다. |
POST | /api/delete | 관리자 | id를 받는다. JSON 클라이언트에는 {"id", "deleted": true}와 200을 반환한다. |
GET /api/video?id=<id>를 조회해 상태가 ready나 failed가 될 때까지 기다린 뒤 watch_url을 열 수 있다. 네트워크에서 자격 증명을 보낼 때는 HTTPS를 사용해야 한다. Go 서버 자체는 HTTP를 제공한다. 세션 쿠키로 로그인하고 동영상을 올리는 curl 예시는 다음과 같다.
curl -c session.txt -H "Content-Type: application/json" \\
-d '{"username":"admin","password":"secret"}' http://127.0.0.1:8081/api/login
curl -b session.txt -H "Accept: application/json" \\
-F title="Demo" -F desc="Recorded meeting" -F category="Meetings" \\
-F file=@demo.mp4 http://127.0.0.1:8081/api/upload
curl "http://127.0.0.1:8081/api/video?id=<id>"
세션 쿠키 대신 API 키를 사용하는 예시는 다음과 같다.
curl -H "Authorization: Bearer tt-..." -H "Accept: application/json" \\
-F title="Demo" -F desc="Recorded meeting" -F category="Meetings" \\
-F file=@demo.mp4 http://127.0.0.1:8081/api/upload
Python requests로 로그인하고 업로드하는 예시는 다음과 같다.
import os
import requests
base = "http://127.0.0.1:8081"
s = requests.Session()
s.post(f"{base}/api/login",
json={"username": "admin", "password": os.environ["ADMIN_PASS"]}).raise_for_status()
with open("demo.mp4", "rb") as f:
r = s.post(f"{base}/api/upload",
headers={"Accept": "application/json"},
data={"title": "Demo", "desc": "Recorded meeting", "category": "Meetings"},
files={"file": ("demo.mp4", f, "video/mp4")})
r.raise_for_status()
video_id = r.json()["id"]
print(requests.get(f"{base}/api/video", params={"id": video_id}).json())
SQLite는 순수 Go 드라이버 modernc.org/sqlite를 사용하므로 단일 실행 파일 빌드에 CGO가 필요하지 않다. STORE=fs로 이전 방식의 파일시스템 JSON 메타데이터를 계속 사용할 수 있다. 두 방식 모두 동영상 파일은 디스크에 둔다. 저장 구조에는 videos/ 아래 SQLite 메타데이터 데이터베이스(tiny-tube.db; WAL/SHM 파일이 함께 생길 수 있음), 동영상 ID별 디렉터리의 original.mp4(또는 .mov, .mkv, .webm), FS 방식의 meta.json(또는 마이그레이션 원본), 성공적으로 변환한 뒤 공개되는 hls-<generated>/index.m3u8과 seg000.ts, 선택적인 poster.jpg 등이 포함된다.
HLS는 높이 720픽셀로 조정한 단일 화질, H.264 동영상, AAC 오디오, 목표 6초 구간을 사용한다. 여러 화질을 전환하는 적응형 스트리밍은 아니다. 변환 뒤에도 원본을 보존한다. 원본 파일은 HTTP Range 요청을 지원하지만 탐색 가능 여부는 파일 배치와 브라우저에도 달려 있다. 원본 MP4에 faststart를 적용하지 않는다. 멀티파트 파일 부분은 메모리를 최대 8 MiB 사용한 뒤 임시 디스크로 넘긴다. 요청 크기 제한은 별도로 http.MaxBytesReader가 적용한다.
루트에 있는 기존 동영상과 index.m3u8, poster.jpg 파일은 계속 호환된다. SQLite 시작 때 데이터베이스에 없는 ID의 기존 meta.json 레코드를 가져온다. 재가져오기는 기존 SQLite 수정을 덮어쓰지 않는다. JSON 파일은 유지되지만 SQLite에서 수정한 내용을 STORE=fs로 되돌려 내보내지는 않는다. 기존 SQLite 데이터베이스는 행을 잃지 않고 변환 오류 메시지를 보존하며 사용자·세션 테이블을 추가하도록 업그레이드된다. FS 방식은 메타데이터를 임시 파일에 쓰고 동기화·닫은 뒤 교체한다. 애플리케이션의 읽기와 쓰기는 프로세스 안에서 동기화한다. 동영상 디렉터리마다 서버 프로세스를 하나만 실행해야 하며 여러 인스턴스가 저장소를 공유하는 것은 지원하지 않는다.
Nginx 배포와 개발
Go 서버만으로 모든 요청을 처리할 수 있다. nginx.sample.conf는 /videos/를 포함한 모든 경로를 Go로 전달해 비공개 파일과 링크 검사가 적용되도록 한다. Range/206 응답과 HLS·포스터 콘텐츠 유형도 유지한다. 이 파일은 완성된 배포 설정이 아니라 출발점이다.
- 예시 호스트 이름과 저장 경로를 바꾸고 TLS 인증서를 설정한다. 설정된 인증서 파일은 Nginx가 시작하기 전에 존재해야 한다.
- 예시는 전체 요청 2 GiB 한도와 300초 비활성 제한 시간을 쓴다.
MAX_UPLOAD_BYTES를 바꾸면 프록시 한도도 일치시킨다. 프록시에서 전체 파일이 한 번 더 복사되지 않도록 업로드 요청 버퍼링은 꺼져 있다. - TLS·인증 라우팅을 프록시에 의존한다면 Go 포트에 직접 접속하지 못하게 제한한다.
- Go가 이미 세션 역할을 검사한다. Nginx에서 업로드 인증을 추가하는 것은 선택 사항이며, 설정하더라도 세션 쿠키가 깨지지 않아야 한다.
- SQLite 파일과 저널은 비공개로 유지한다. 예시는 데이터베이스 확장자와 스테이징 경로를 차단하고 Go도 파일 식별자를 확인해 데이터베이스 별칭을 거부한다. 업스트림은 기본 포트 8081을 사용하므로
ADDR나PORT를 바꾸면 함께 수정한다.
처리 용량은 측정되지 않았다. 배포 기기에서 설정을 수정한 뒤 nginx -t로 검증하고, 실제 인증된 업로드와 재생을 확인해야 한다.
소스에서 실행하거나 빌드하려면 패치된 Go 1.26.10 또는 1.27.3 이상을 설치한다. go.mod는 Go 1.27.3을 선택한다. .env.example을 .env로 복사하고 설치 안내대로 첫 관리자 비밀번호를 설정한다. 저장소 디렉터리에서 go run .을 실행하거나 다음 명령으로 빌드한다.
# Windows PowerShell
go build -o tiny-tube.exe .
.\\tiny-tube.exe
# Linux / macOS
go build -o tiny-tube .
./tiny-tube
Windows에서는 dev.bat(PowerShell에서는 .\\dev.bat)으로 소스 파일, 자산, 카탈로그, 모듈 파일, .env가 바뀔 때 애플리케이션을 다시 빌드하고 시작할 수 있다. 관리 CLI에는 cli.bat이나 서버와 동일한 저장소 설정을 적용한 go run ./cmd/cli를 사용한다. testdata/의 추적 동영상은 테스트용 파일이며 라이브러리 콘텐츠가 아니다.
Windows PowerShell에서 Linux용 바이너리를 교차 빌드하려면 대상 아키텍처를 지정한다.
$env:GOOS = "linux"
$env:GOARCH = "amd64" # or arm64
go build -o tiny-tube .
Remove-Item Env:GOOS, Env:GOARCH
GitHub Actions는 모든 푸시와 풀 리퀘스트에서 CI를 실행하며 Actions 탭에서 수동 실행할 수도 있다. Linux와 Windows에서 Go 테스트를 진행하고 실제 FFmpeg를 설치해 HLS·포스터 테스트를 수행한다. Linux 레이스 검사, 캐시를 사용하지 않고 순서를 섞은 테스트, 7일간 보존하는 커버리지 보고서, 두 플랫폼의 CGO 없는 빌드도 실행한다. 변경된 Go 파일에는 범위를 제한한 gofmt 정책을 적용하고 go vet, 모듈 체크섬 확인, go mod tidy -diff를 실행한다. Node UI 동작 테스트와 포함된 JavaScript 문법 검사도 한다. 고정한 govulncheck가 최신 취약점 데이터베이스로 Linux·Windows·macOS를 검사한다. 데이터베이스나 도구 오류는 통과로 처리하지 않고 작업 실패로 처리한다.
v1.0이나 v1.0.1 같은 버전 태그를 푸시하면 먼저 같은 검사를 수행한 뒤 CGO 없는 플랫폼 패키지 6개를 교차 빌드하고 모든 아카이브와 SHA256SUMS를 GitHub Release에 게시한다. 검사나 플랫폼 빌드에 실패하면 릴리스 게시가 막힌다. Linux와 Windows 테스트는 해당 플랫폼에서 실행하고 macOS 및 ARM 패키지는 교차 컴파일하지만 모든 대상에서 네이티브 실행 테스트를 하지는 않는다. 일반 브랜치 푸시와 풀 리퀘스트는 릴리스를 게시하지 않는다.
Actions는 커밋 SHA로 고정하고 저장소 권한은 기본 읽기 전용이며 작업 시간 제한을 둔다. 같은 브랜치의 이전 실행은 새 실행으로 대체되면 취소된다. CI는 go.mod에서 선택한 Go 도구 체인을 사용한다. 마지막 릴리스 작업에만 다운로드 게시 권한이 있으며 애플리케이션을 배포하지 않는다. 필수 검사와 브랜치 보호는 저장소 설정에서 별도로 지정해야 한다.
테스트와 빌드 기본 명령은 다음과 같다.
go test ./...
go vet ./...
go build ./...
테스트는 동시 업로드, 요청 제한, 정리, 메타데이터 교체, 인증, HTTP 재생과 Range 지원, 제한된 변환 동시 실행, 종료·재시작 복구, 두 저장 방식, 스키마·가져오기 마이그레이션, 변환 중 수정·삭제를 다룬다. 실제 FFmpeg 업로드부터 HLS까지의 통합 테스트는 PATH에서 FFmpeg를 찾을 수 있을 때 실행하고, 그렇지 않으면 건너뛴다.
GET /healthz는 생존 여부를 보고한다. GET /readyz는 쓰기 가능한 저장소와 읽을 수 있는 메타데이터를 확인하고, 둘 중 하나라도 실패하면 503을 반환한다. FFmpeg 코덱이나 디스크 용량을 보증하지는 않는다. 수동 확인에서는 검색, 인증된 업로드, 변환, 브라우저 재생을 점검한다. 저장소를 변경하는 점검에는 임시 VIDEO_DIR을 사용한다. 기여 안내는 AGENTS.md에 있다. 실행 중 업로드한 파일은 Git에서 무시하며 testdata/의 테스트 파일만 추적한다.
동영상 파일과 SQLite 데이터베이스를 백업해야 하며 사용자 지정 외부 DB_PATH도 포함해야 한다. 데이터베이스를 복사하기 전에는 서버를 중지하거나 SQLite 백업 기능을 사용한다. 실행 중인 .db 파일만 복사하면 아직 체크포인트되지 않은 WAL 변경 내용이 빠질 수 있다. 내장 할당량·복구·백업 기능은 없다. HTML 템플릿, 스타일, 스크립트는 web/에 있고 Go 실행 파일에 포함된다. 프런트엔드 패키지 설치나 별도 자산 배포는 필요하지 않으며, 파일을 수정하면 서버를 다시 빌드하고 시작해야 한다.
병합 뒤 보안 점검과 배포 제한은 보안 문서에 기록돼 있다. 동영상·데이터베이스 저장소는 서비스 계정만 쓸 수 있게 하고, FFmpeg는 운영체제에서 최소 권한으로 실행하며, 네트워크 접근에는 HTTPS를 사용하고 기존 바이너리도 선택한 패치 도구 체인으로 다시 빌드해야 한다. 추가 수정 사항은 2026-10-24 검토 및 해결 기록에 있다. 클라이언트 동작 테스트는 node --test tests/ui.test.cjs로 실행한다. Node는 테스트 도구이며 실행 의존성은 아니다.
Tiny Tube는 MIT License로 제공된다. 포함된 hls.js 플레이어는 Apache-2.0 라이선스를 적용받으며, 다른 종속성은 각각의 라이선스를 따른다.
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요