앱·클라우드 서버
939ee85 기준입니다.
Ente는 사진·인증 코드·문서 등을 기기에서 암호화한 뒤 클라우드에 보관하고 동기화하는 서비스와 앱들의 모음입니다.
대표 제품 Photos는 사진 백업·앨범·공유를, Auth는 2단계 인증 코드를, Locker는 중요한 문서와 자격 증명 보관을 다룹니다.
루트 README.md가 소개하는 세 제품이며, 실제 디렉토리에는 Ensu, Cast, Space 등 더 넓은 개발 영역도 보입니다.
이 저장소의 핵심은 서버에 데이터를 맡기면서도, 내용의 복호화 권한은 사용자의 기기에 두는 설계입니다. 서버는 계정 확인·접근 권한·저장 용량·암호문 위치를 관리합니다. 사진 내용을 이해하지 않아도 이 업무는 할 수 있습니다. 반대로 사진을 화면에 표시하려면 클라이언트가 키를 풀고 데이터를 복호화해야 합니다.
창고 직원은 상자 번호와 무게를 알고, 누구에게 상자를 내줄지 확인합니다. 하지만 안에 든 사진을 볼 열쇠는 없습니다. 앱은 상자를 열어 사진을 보여 주고, 공유할 때는 상대방이 열 수 있도록 열쇠를 다시 감쌉니다.
이 비유에서 창고 관리자는 Go 서버 Museum, 상자 보관소는 S3 호환 저장소, 상자 장부는 PostgreSQL, 열쇠를 다루는 쪽은 모바일·웹·데스크톱·CLI 클라이언트입니다.
이 흐름은 web/packages/gallery/services/upload/upload-service.ts의 encryptFile()과
server/pkg/api/file.go의 CreateOrUpdate()로 연결해 읽을 수 있습니다.
화면을 그리는 코드보다 이 경로를 먼저 따라가면 제품의 중심을 빨리 잡을 수 있습니다.
키의 계층, 네트워크 실패, 중복 실행, DB 트랜잭션, 언어 간 데이터 변환이 한 사용자 행동에 모입니다. 글을 마칠 때에는 “업로드 성공”이 단순히 HTTP 요청 하나의 성공과 왜 다른지 설명하는 것이 목표입니다.
2026-10-23 GitHub Trending Dart Daily 1위라는 순위는 관심의 신호입니다. 다만 순위만으로 유입 경로나 인기의 원인을 알 수는 없습니다. 기능·공개된 구현·최근 변화는 관심을 끌 만한 요소지만, 특정 사건이 순위를 만들었다고 단정할 근거는 아닙니다.
사진 백업에는 자동 업로드, 여러 기기, 공유, 검색이 필요합니다. 여기에 서버가 사진을 복호화할 수 없다는 제약을 더하면, 썸네일 생성과 내용 기반 분석을 어디서 할지부터 다시 정해야 합니다. Ente에서는 클라이언트 암호화 코드와 로컬 ML 연결 코드를 함께 읽을 수 있습니다.
루트 README는 Photos를 Apple·Google Photos의 대안으로 소개합니다. 아래 표는 개별 경쟁 제품의 최신 요금이나 설정을 판정하는 비교가 아니라, 그런 대안을 고를 때 살펴볼 설계 방식의 비교입니다. Ente 열은 커밋 939ee85 기준입니다.
| 비교 축 | 다른 접근에서 확인할 점 | Ente에서 보이는 장점과 비용 |
|---|---|---|
| 일반 사진 클라우드 | 편리한 백업·공유와 함께, 복호화 키를 누가 통제하는지 확인 | 업로드 전 암호화와 키 계층을 공개 코드로 추적 가능. 키 복구 설계가 더 복잡해짐 |
| 서버 분석형 사진 서비스 | 서버가 원본을 분석하면 서버 연산 자원과 원본 접근 권한이 필요 | 기기에서 분석하는 경로가 있음. 대신 기기 성능·배터리·모델 관리 부담이 생김 |
| 단순 암호화 파일 보관 | 암호문 저장 외에 앨범·공유·썸네일·동기화를 직접 마련해야 하는지 확인 | 사진 제품 기능과 암호화 계층을 함께 제공. 구현과 운영 부품 수가 많음 |
| 직접 만든 백업 서버 | 클라이언트별 유지보수와 내보내기 경로까지 만들 수 있는지 확인 | 모바일·웹·데스크톱·CLI 소스를 함께 학습 가능. 전체를 빌드하려면 여러 도구 체인이 필요 |
| 자체 호스팅 | API뿐 아니라 DB·객체 저장소·백업까지 운영 가능한지 확인 | Compose와 관리 문서가 있음. 서버 README는 자체 호스팅 지원의 제약도 명시 |
Ente의 학습 가치는 암호화 알고리즘 하나보다, 암호화를 유지한 채 제품의 편의 기능을 연결하는 과정에 있습니다. 예를 들어 앨범 공유는 단순히 DB의 공개 여부를 바꾸는 일이 아닙니다. 상대방에게 컬렉션 키를 안전하게 전달하는 문제까지 포함합니다.
docs/docs/cli/migration.md는 새 CLI가 2026년 10월 0.4.0부터 도입됐고,
기존 Go CLI는 전환기에 계속 지원하되 새 개발은 Rust CLI에서 진행한다고 설명합니다.
2026-10-23 GitHub 기준 최신 릴리스는 ente-cli-v0.4.0입니다. 커밋 939ee85의
rust/apps/cli/Cargo.toml은 이미 0.4.1을 선언합니다. 발표 버전과 개발 버전이 다른 정상적인 사례입니다.
기존 cli/README.md의 ente account add와 새 문서의
ente account login --app photos는 서로 다른 세대의 명령입니다.
검색으로 찾은 명령이 어느 구현을 가리키는지 확인하는 습관도 배울 수 있습니다.
이 변화가 당일 트렌딩의 직접 원인인지는 확인되지 않았습니다.
모노레포는 공장 단지와 비슷합니다. 간판은 Ente 하나지만 사진 앱 생산 라인과 인증 앱 생산 라인이 있고, 공용 부품 창고와 배송 관리실을 함께 씁니다. 특정 제품의 새 버전이 공장 전체의 모든 제품 버전을 뜻하지는 않습니다.
| 영역 | 실제 선언 | 읽을 파일과 역할 |
|---|---|---|
| 모바일 언어 | Dart SDK >=3.10.0 <4.0.0 | mobile/pubspec.yaml: Photos·Auth·Locker와 공통 패키지를 workspace로 묶음 |
| 모바일 UI | CI Flutter 3.47.2 | .github/actions/setup-flutter/action.yml: SDK 버전과 OS별 다운로드 해시 고정 |
| 모바일 통신·저장 | dio 5.9.2, sqlite_async 0.11.7, flutter_secure_storage 9.0.0 | mobile/apps/photos/pubspec.yaml: HTTP·로컬 DB·비밀 저장소의 서로 다른 책임 |
| 모바일과 Rust 연결 | flutter_rust_bridge 2.13.0 | 같은 pubspec 및 rust/Cargo.toml: Dart/Rust 양쪽 버전을 맞춤 |
| 웹 화면 | React 19.3.0, Next.js 16.4.2, MUI 9.4.0 | web/apps/photos/package.json, web/packages/base/package.json: 앱과 공용 UI 기반 |
| 웹 암호화·작업 분리 | libsodium-wrappers-sumo 0.7.15, comlink 4.4.2 | web/packages/base/package.json: 암호화 라이브러리와 Worker 호출 연결 |
| 데스크톱 | Electron 42.10.0, electron-builder 26.15.3 | desktop/package.json: 웹 Photos 화면을 렌더러로 재사용하고 네이티브 기능을 연결 |
모바일 Photos 앱 선언 버전은 1.3.67+2158, 데스크톱 패키지는 1.7.31-beta입니다.
앱 버전·라이브러리 버전·SDK 제약은 서로 다른 숫자입니다.
Flutter 버전은 앱 pubspec의 sdk: flutter만으로 알 수 없으므로 CI 설정까지 읽어야 합니다.
mobile/pubspec.lock, web/package-lock.json, rust/Cargo.lock을 함께 보세요.
이 글의 버전 표는 명시한 manifest·CI 선언 기준이며, 설치 결과나 최신 공개 버전을 뜻하지 않습니다.| 기술 | 버전·설정 | 사용 이유와 근거 |
|---|---|---|
| Go | go 1.26.0, toolchain go1.26.10 | server/go.mod: Museum 서버. 기존 cli/go.mod도 같은 Go 선언 |
| Gin·PostgreSQL 연결 | gin v1.9.1, lib/pq v1.8.0 | server/go.mod: HTTP 처리와 SQL 드라이버. 저장 계층은 직접 SQL을 작성 |
| 설정·마이그레이션 | viper v1.8.1, migrate/v4 v4.12.2 | server/go.mod: 설정 병합과 DB 스키마 변경 이력 |
| 객체 저장소 SDK | aws-sdk-go v1.34.13 | server/go.mod: S3 호환 API와 서명된 URL 생성 |
| Rust | edition = "2024", workspace resolver = "2" | rust/Cargo.toml: 여러 앱·공통 crate·바인딩을 한 workspace로 관리 |
| Rust 실행·통신·DB | tokio 1.53.1, reqwest 0.12.28, rusqlite 0.40.2 | rust/Cargo.toml: 비동기 작업, HTTP, 로컬 SQLite 기반 |
| Rust 암호화 | argon2 0.6.0, crypto_secretbox 0.1.0, crypto_secretstream =0.2.0 | rust/crates/core/Cargo.toml: 비밀번호 기반 키 유도와 libsodium 호환 암호문 처리 |
| Rust ML | ort =2.0.0-rc.13 | rust/Cargo.toml: ONNX Runtime 연결. 릴리스 후보 버전까지 정확히 고정 |
| ML 연구·검증 | Python >=3.12, onnxruntime==1.28.0, numpy>=2.1.2 | infra/ml/pyproject.toml: 모델 실험·검증 영역. Museum의 Python 백엔드라는 뜻은 아님 |
같은 저장소에 있다고 해서 모든 앱이 모든 기술을 실행하는 것은 아닙니다.
예를 들어 Rust CLI는 SQLCipher를 활성화한 로컬 저장소와 OS 키링을 사용하도록 의존성을 선언하지만,
서버 PostgreSQL과 역할이 다릅니다. rust/apps/cli/Cargo.toml의 feature 목록이 이 차이를 드러냅니다.
server/Dockerfile은 golang:1.26.10-alpine3.23에서
CGO_ENABLED=0으로 빌드하고 alpine:3.22에 실행 파일과 리소스를 복사합니다.server/compose.yaml은 Museum, postgres:15,
pgsty/silo 객체 저장소, alpine/socat을 연결합니다. 일부 이미지 태그는 고정되지 않아 완전한 재현성은 별도 과제입니다.web/package.json은 npm workspace, npm@11.12.1,
typescript 6.0.3을 선언합니다. build:photos는 필요한 WASM 빌드 뒤 Next 빌드를 진행합니다.web/Dockerfile은 앱별 out 결과를 Nginx 이미지에 복사합니다.
이 경로에서는 Next 개발 서버를 그대로 운영하는 구조가 아닙니다..github/workflows/web-lint.yml은 Node 24에서 설치·감사·빌드·검사·테스트를,
server-lint.yml은 PostgreSQL을 쓰는 테스트를 구성합니다.모바일 Photos의 computer, flutter_sodium은 Git 커밋으로 고정된 의존성입니다.
mobile/pubspec.yaml에는 캐시 삭제·비밀 저장소·동영상 문제에 대한 override와 이유도 적혀 있습니다.
패키지 이름뿐 아니라 “왜 이 버전에 머무르는가”까지 읽는 것이 유지보수 공부입니다.
일반적인 서명 URL 업로드 경로를 단순화한 도식입니다. 모든 요청이 같은 경로를 쓰는 것은 아닙니다.
서버는 파일 내용을 읽지 않아도 소유권·용량·업로드 완료 여부를 검사할 수 있습니다.
server/ente/file.go의 File에는 OwnerID, CollectionID,
EncryptedKey, KeyDecryptionNonce가 함께 들어갑니다.
architecture/README.md가 설명하는 키 계층은 업로드 구현에서도 확인됩니다.
web/packages/gallery/services/upload/upload-service.ts의 encryptFile()은
generateBlobOrStreamKey()로 파일 키를 만들고, 마지막에 encryptBox(fileKey, collectionKey)를 호출합니다.
// encryptFile()의 실제 호출을 뽑아 흐름만 줄인 예시
const fileKey = await worker.generateBlobOrStreamKey();
const encryptedMetadata = await worker.encryptMetadataJSON(
metadata,
fileKey,
);
const encryptedFileKey = await worker.encryptBox(fileKey, collectionKey);
// 원본 함수는 사진 본문·썸네일·추가 메타데이터도 처리합니다.
모바일에서도 mobile/apps/photos/lib/module/upload/service/file_uploader.dart의
_tryToUpload() 안에 파일 키와 컬렉션 키를
CryptoUtil.encryptSync()로 연결하는 코드가 있습니다.
새 파일 등록 전에 decryptVerify(..., chunkLimit: 1)로 첫 청크를 대상으로 검증하는 경로도 보입니다.
이것을 파일 전체를 매번 검증한다는 의미로 확대하면 안 됩니다.
복호화 쪽은 rust/crates/collections/src/lib.rs의 open_collection_key()가 좋은 입구입니다.
내가 소유한 컬렉션이면 secretbox::decrypt()와 session.master_key를,
공유받은 것이면 sealed::open()과 내 비밀키를 사용합니다. 같은 반환 타입 Key가 하위 계층을 단순하게 만듭니다.
secretbox.rs의 encrypt()는 Nonce::generate()를 호출해 암호문과 nonce를 함께 반환합니다.rust/crates/core/src/crypto/stream.rs는 ENCRYPTION_CHUNK_SIZE = 4 * 1024 * 1024,
즉 4 MiB 단위 처리를 정의합니다. Encryptor는 마지막 조각에 Tag::Final을 붙이고,
Decryptor::finish()는 그 표식을 못 봤으면 Error::StreamTruncated를 반환합니다.
// rust/crates/core/src/crypto/stream.rs: 실제 메서드
pub fn finish(self) -> Result<()> {
if self.seen_final {
Ok(())
} else {
Err(Error::StreamTruncated)
}
}
조각마다 인증에 성공해도 마지막 몇 조각이 사라질 수 있습니다.
그래서 “읽을 데이터가 더 없다”와 “정상적인 마지막 표식을 확인했다”를 구분합니다.
이 파일의 test_decrypt_file_truncation_at_chunk_boundary() 같은 테스트 이름만 읽어도 중요한 실패 조건을 배울 수 있습니다.
CreateOrUpdate()는 file.ID == 0이면 생성, 아니면 갱신으로 보냅니다.
GetUploadURLWithMetadata()는 양수인 ContentLength, 허용 크기, MD5 값, 사용량을 확인합니다.
기존 묶음 API의 GetUploadURLs()에는 MaxUploadURLsLimit = 50 제한도 있습니다.
이 50은 사진 총개수 제한이 아니라 해당 API가 한 번에 발급할 URL 수의 제한입니다.
FileRepository.Create()는 BeginTx()로 시작하고 defer tx.Rollback()을 둡니다.
파일 행, 앨범과 파일의 연결, 객체 위치, 사용량, 복제 필요 기록을 처리한 뒤 Commit()합니다.
main()은 이 repository와 controller를 구조체 필드로 조립합니다.
이름만으로 완벽한 계층 분리를 가정하기보다 실제 의존 방향을 보는 것이 좋습니다.
객체 업로드와 DB 등록은 서로 다른 작업이므로, 실패한 중간 상태를 정리하는 설계까지 있어야 업로드가 완성됩니다.
server/pkg/controller/file.go의 getObjectURL()은 임시 객체 정보를 다루고,
repository의 생성 경로는 ObjectCleanupRepo.RemoveTempObjectKey()와
markAsNeedingReplication()을 같은 DB 트랜잭션 안에서 호출합니다.
DB 트랜잭션이 외부 객체 저장소까지 함께 되돌린다고 이해하면 안 됩니다.
mobile/apps/photos/lib/services/sync/sync_service.dart의 SyncService.sync()는
이미 실행 중이면 _existingSync!.future를 반환합니다.
새 동기화를 여러 개 만들지 않고 한 작업의 결과를 공유하는 방식입니다.
성공 여부와 관계없이 finally에서 completer를 완료하고 참조를 비웁니다.
// sync()의 중복 실행 처리 부분: 실제 소스 발췌
if (_existingSync != null) {
_logger.warning("Sync already in progress, skipping.");
return _existingSync!.future;
}
_syncStopRequested = false;
_existingSync = Completer<bool>();
연결 상태 변화, 계정 설정, 권한 허용 등 여러 사건이 동기화를 요청할 수 있습니다.
이 보호는 같은 SyncService 인스턴스의 실행 겹침을 다루며, 모든 기기의 분산 잠금을 뜻하지 않습니다.
이벤트 버스는 화면에 진행 상황을 알리는 통로이고 서버가 보내는 메시지 큐와도 구분해야 합니다.
remote_sync_service.dart의 _syncCollectionDiff(collectionID, sinceTime)는
getEncryptedFilesDiff()를 호출하고 삭제·갱신 내역을 반영합니다.
이어 latestUpdatedAtTime을 저장하고 hasMore이면 다음 페이지를 가져옵니다.
체크포인트를 언제 갱신하는지, 삭제가 어떤 형태로 전달되는지를 함께 읽어 보세요.
매번 도서관의 모든 책을 새로 복사하는 대신 “마지막 장부 확인 이후 바뀐 책” 목록을 받습니다. 체크포인트는 마지막으로 읽은 장부 위치입니다. 장부 위치만 먼저 옮기고 변경을 저장하지 못하면 다음 조회에서 빠뜨릴 수 있습니다.
rust/bindings/frb/photos/src/api/ml_indexing_api.rs의 analyze_image_rust()는
요청을 공통 형태로 바꾸고 spawn_blocking_with()에서 indexing::analyze_image()를 실행합니다.
rust/bindings/napi/photos/src/lib.rs의 AnalyzeImageTask::compute()도 같은 공통 분석 함수를 부릅니다.
Flutter와 Electron의 호출 방식은 달라도 분석 엔진을 함께 쓸 수 있는 구조입니다.
웹은 또 다른 경계가 있습니다. web/packages/base/crypto/index.ts의
createComlinkCryptoWorker()가 Worker를 만들고, web/packages/base/crypto/worker.ts의 CryptoWorker가
libsodium 함수들을 노출합니다. 암호화 작업이 화면의 주 실행 흐름을 오래 붙잡지 않도록 하는 구성입니다.
Rust의 WASM 바인딩도 있지만 웹 암호화가 전부 Rust로 바뀐 것은 아닙니다.
rust/bindings/wasm/photos/Cargo.toml은 공통 WASM 모듈의 기능을 선택하고,
앞서 본 웹 업로드 경로는 CryptoWorker를 사용합니다.
공통화된 부분과 아직 별도로 유지되는 부분을 함께 읽는 것이 실제 모노레포 분석입니다.
mobile/apps/photos와 web/apps/photos는 같은 제품의 다른 클라이언트입니다.
반면 server/pkg/api와 server/pkg/repo는 같은 서버의 다른 계층입니다.
이 두 분류를 혼동하면 “사진 코드가 왜 여러 군데 있지?”라는 의문이 생깁니다.
| 순서 | 파일 | 답해 볼 질문 |
|---|---|---|
| 1 | architecture/README.md | 각 키는 무엇을 잠그고, 어디에 암호화된 형태로 저장되는가? |
| 2 | web/packages/gallery/services/upload/upload-service.ts | encryptFile()의 입력과 반환값에 평문 키가 어떻게 구분되는가? |
| 3 | server/ente/file.go | 서버가 받는 파일 레코드에는 어떤 필드가 보이는가? |
| 4 | server/pkg/api/file.go → server/pkg/controller/file.go → server/pkg/repo/file.go | 입력 해석·규칙 검사·SQL 실행은 어디서 나뉘는가? |
| 5 | mobile/apps/photos/lib/services/sync/sync_service.dart | 같은 작업이 동시에 요청되면 어떤 Future를 돌려주는가? |
| 6 | rust/crates/core/src/crypto/stream.rs | 정상 종료, 손상, 잘림을 어떻게 구분하는가? |
그다음 mobile/apps/photos/lib/db에서 기기의 장부를,
server/migrations에서 서버 장부의 변경 이력을 보세요.
양쪽 모두 파일을 다루지만 로컬 파일과 서버 파일을 연결해야 하는 문제와 계정 전체의 권한을 관리하는 문제는 다릅니다.
rust/bindings/frb/photos/src/lib.rs에는 frb_generated 모듈이 연결됩니다.
처음부터 생성 코드 전체를 해독할 필요는 없습니다.
rust/bindings/frb/photos/src/api/ml_indexing_api.rs에서 요청 타입, 공통 함수 호출, 오류 변환부터 읽으면 경계를 이해하기 쉽습니다.
Future는 나중에 끝나는 결과이고 Completer는 그 결과를 직접 완료시키는 도구입니다.
SyncService.sync()에서 completer를 저장해 두는 이유는 여러 요청이 같은 진행 중 작업을 가리키게 하기 위해서입니다.
finally가 빠지면 예외 이후에도 “실행 중” 상태가 남을 수 있다는 점을 생각해 보세요.
_registerListeners()는 연결 상태·계정 설정·구독 변경·동기화 상태를 듣습니다.
실습으로 각 이벤트의 발생 시점과 sync() 진입 조건을 표로 그려 보세요.
UI 버튼 하나만 생각하는 예제보다 실제 앱에서 왜 중복 방지가 필요한지 잘 드러납니다.
FileHandler.CreateOrUpdate()에서 JSON 바인딩과 서버 시각 부여를 찾고,
FileController.GetUploadURLWithMetadata()에서 용량 제한을 찾으세요.
실습은 “크기 0, 허용 크기 초과, 잘못된 MD5, 잔여 용량 부족”을 각각 어느 조건이 거절하는지 연결하는 것입니다.
server/pkg/middleware/auth.go의 TokenAuthMiddleware()도 같이 읽으세요.
토큰 검사와 앱 구분 뒤 사용자 ID를 요청 헤더에 설정합니다.
로그인한 사용자라는 사실과 특정 앨범을 수정할 수 있다는 권한을 별도로 확인해야 하는 이유를 설명해 보세요.
FileRepository.Create()는 파일 행 하나만 삽입하지 않습니다.
앨범 연결, 객체 위치, 사용량도 일치해야 합니다.
실습으로 SQL 단계마다 실패 표시를 놓고, Rollback()이 어떤 DB 변경을 되돌리는지 적어 보세요.
이미 S3에 올라간 암호문은 이 목록에 들어가지 않습니다.
rust/crates/core/src/crypto/secret.rs의 SecretVec·SecretString은
Zeroize와 ZeroizeOnDrop을 사용하고, 디버그 출력은 비밀 대신 [REDACTED]를 반환합니다.
into_vec()처럼 일반 값으로 꺼내는 경로도 있으므로 모든 복사본이 자동으로 지워진다고 확대해서는 안 됩니다.
실습으로 secretbox.rs의 짧은 암호문 거절, 잘못된 키 테스트,
stream.rs의 잘림 테스트를 읽으세요.
같은 결과를 다시 복호화하는 정상 사례뿐 아니라, 어떤 입력을 거절해야 하는지가 라이브러리 계약의 절반입니다.
web/packages/base/crypto/index.ts는 Worker 안인지 확인해 직접 호출과 원격 호출을 나눕니다.
Comlink는 다른 실행 흐름의 함수를 비동기 함수처럼 부르게 돕습니다.
화면이 멈추지 않는다는 이점과 별개로 데이터 전달·복사·오류 전달 비용은 남습니다.
실습으로 Flutter의 AnalyzeImageRequest와 N-API의 같은 목적 요청을 비교하세요.
전자는 image_path로 파일 위치를, 후자는 image_bytes로 바이트를 받는 경로가 있습니다.
공통 분석 엔진을 재사용하더라도 플랫폼에 맞춘 입력 변환이 필요한 이유를 설명할 수 있습니다.
ml_indexing_api.rs의 init_ml_runtime()과 analyze_image_rust()는
ensure_indexing_models()를 거쳐 모델 경로를 확보합니다.
요청의 run_faces, run_clip, run_pets로 분석 종류를 선택합니다.
모델 다운로드와 분석 실행은 서로 다른 실패를 가진 작업입니다.
실습은 결과의 얼굴 목록과 CLIP 임베딩을 구분해 표로 정리하는 것입니다.
사진을 숫자 벡터로 바꾸는 단계와, 벡터를 저장·검색하는 단계도 다릅니다.
rust/crates/photos/src/ml_db와 rust/crates/vecdb가 다음으로 읽을 곳입니다.
.github/workflows/rust-test.yml은 workspace와 CLI를 별도 suite로 둡니다.
주석은 SQLCipher feature가 다른 SQLite 테스트에 합쳐지지 않도록 분리한다고 설명합니다.
cargo test --locked와 실행 대상 분리는 단순 속도 조절을 넘어 의존성 조합을 통제하는 역할을 합니다.
한 기능을 배울 때는 정상 동작 하나와 실패 조건 하나를 반드시 짝지어 읽으세요. 동기화에는 중복 요청, 암호화에는 잘림, DB에는 부분 실패, 모델에는 다운로드 실패를 붙이면 기술 이름을 외우는 공부에서 시스템을 이해하는 공부로 넘어갈 수 있습니다.
docs/docs/self-hosting/installation/requirements.md는 quickstart 클러스터에
CPU 1코어, RAM 1 GB를 최소 조건으로 적습니다.
이 수치는 공식 문서 기준이며, 대량 라이브러리의 처리량을 측정한 벤치마크나 소스 빌드 권장 사양은 아닙니다.
| 항목 | 확인된 조건 | 초보자가 구분할 점 |
|---|---|---|
| 서버 OS | Linux 또는 Unix 계열. 문서는 Ubuntu·Debian 권장 | Docker의 파일 권한·네트워크 동작도 운영 조건에 포함 |
| 컨테이너 | 문서의 Compose 설치 경로는 Docker와 Compose plugin 필요 | Compose 2.30 이상. docker compose 명령 기준 |
| 파일 시스템 | PostgreSQL 컨테이너는 사용자·그룹 권한을 지원하는 저장소 필요 | 문서는 ZFS·EXT4·BTRFS 등을 예시로 제시 |
| 스토리지 | PostgreSQL 데이터와 S3 호환 객체 저장소 모두 필요 | 사진 원본뿐 아니라 DB·썸네일·복제·백업 용량도 계산 |
| 로컬 포트 | server/compose.yaml: API 8080, 객체 API 3200 | 컨테이너 안의 localhost와 사용자 기기의 localhost는 다른 주소 공간 |
| 모바일 Photos | Android minSdk = 26, iOS 15.1 | mobile/apps/photos/android/app/build.gradle과 mobile/apps/photos/ios/Podfile 기준. 다른 앱 전체에 같은 값을 적용할 수 없음 |
모바일 최소 버전은 Photos 앱의 설정 기준입니다. 서버 README에는 Docker 없이 단일 바이너리로 실행하는 선택지도 있으므로, Docker가 Go 서버 자체의 필수 런타임이라는 뜻은 아닙니다. 다만 DB와 객체 저장소는 별도로 준비해야 합니다.
rust/crates/core/src/crypto/argon.rs의 기본 상수는
INTERACTIVE 64 MiB·2회, MODERATE 256 MiB·3회, SENSITIVE 1 GiB·4회입니다.
그런데 derive_sensitive_adaptive()는 실제로 256 MiB·16회에서 시작하고,
실패하면 메모리를 절반으로 줄이고 반복 횟수를 늘려 128 MiB까지 시도합니다.
이는 architecture/README.md의 “SENSITIVE 한계에서 시작해 조절”이라는 개념 설명보다 구체적인 구현입니다.
상수 SENSITIVE가 존재한다고 실제 시작 메모리가 언제나 1 GiB인 것은 아닙니다.
또한 이 비용은 키 유도 작업의 값이지 로그인 전체나 앱 전체의 메모리 상한이 아닙니다.
자물쇠 열쇠를 만들 때 넓은 작업대를 쓰면 반복 작업을 적게 하고, 작업대가 작으면 더 여러 번 작업하는 방식입니다. 구현은 메모리와 반복 횟수의 곱을 유지하도록 조절하지만, 그 곱 하나로 모든 환경의 공격 비용이 같다고 증명되는 것은 아닙니다.
저장소에는 모든 제품을 한꺼번에 개발하는 PC의 공식 최소 RAM이 명시돼 있지 않습니다. 다음 수치는 학습을 위한 여유 자원 제안이며 측정된 공식 요구사항이 아닙니다. 소스 읽기만 할 때에는 전체 SDK를 설치할 필요가 없습니다.
| 학습 범위 | 준비할 도구 | 자원 계획 |
|---|---|---|
| 코드 추적 | 편집기, Git, 검색 도구 | 일반 개발 PC로 시작. 대형 생성 파일을 한 번에 모두 열지 않기 |
| 서버 개발 | Go 1.26 계열, DB·객체 저장소 또는 Docker | RAM 4 GB 이상 여유를 잡는 학습 계획. 최소 운영치와 구별 |
| 웹·Rust 개발 | Node 24 계열, npm, Rust와 WASM 대상 | RAM 8~16 GB, 수십 GB 디스크 여유를 고려. 캐시·도구 체인 크기에 따라 변동 |
| 모바일·ML 개발 | Flutter, 대상 플랫폼 SDK, Rust, 모델 자산 | RAM 16 GB 이상을 계획하면 에뮬레이터 병행에 유리. iOS 빌드는 macOS·Xcode 필요 |
rust/crates/ml/Cargo.toml에는 OS별 Core ML·WebGPU·XNNPACK 관련 feature가 나뉩니다.
이것이 서버에 GPU가 필수라는 뜻은 아닙니다.
분석 장치와 모델, 활성화된 실행 경로에 따라 실제 성능이 달라지므로 특정 초당 처리량은 제시하지 않습니다.
docs/docs/self-hosting/administration/backup.md는 암호문, DB의 암호화된 키·연결 정보,
사용자 측 키 복구 수단을 함께 고려하라고 설명합니다.
객체 저장소의 3개 버킷이 같은 로컬 디스크에 있다면 독립된 3개 클라우드 백업이 아닙니다.
server/compose.yaml도 운영용이 아닌 예제임을 명시합니다.
첫 실습부터 계정이나 실제 사진을 준비할 필요는 없습니다. 읽은 코드의 입력·출력·실패 조건을 작은 결과물로 남기면 됩니다. 실습의 완료 기준은 코드를 많이 읽는 것이 아니라, 특정 실패 상황에서 데이터가 어떻게 남는지 설명하는 것입니다.
읽을 곳: architecture/README.md, server/ente/file.go,
web/packages/gallery/services/upload/upload-service.ts의 encryptFile().
완료 기준: EncryptedKey와 KeyDecryptionNonce가 왜 별도 필드인지,
OwnerID가 보이는 것이 사진 내용의 평문 저장과 왜 다른지 설명할 수 있습니다.
읽을 곳: mobile/apps/photos/lib/services/sync/sync_service.dart의
sync(), existingSync(), finally 블록.
_existingSync 값과 공유되는 Future를 적습니다.완료 기준: A와 B는 진행 중 작업을 공유하고 C는 새 작업을 시작한다는 표를 만듭니다.
일반 catch의 예외 재전파와 complete(successful)의 불리언 전달이 어떻게 다른지도 확인하세요.
덧붙여 existingSync()의 주석이 왜 일반적인 대기 용도를 경계하는지 적어 보세요.
읽을 곳: rust/crates/core/src/crypto/stream.rs의
StreamingEncryptor::finish(), estimate_encrypted_size(), 잘림 테스트.
아래는 암호화를 실행하지 않고 청크 수만 계산하는 독립 예제입니다. 0바이트에도 인증을 위한 마지막 청크가 필요하다는 점을 반영합니다.
# 저장소 실행과 무관한 경계값 계산 예제
CHUNK = 4 * 1024 * 1024
for size in (0, CHUNK - 1, CHUNK, CHUNK + 1):
chunks = max(1, (size + CHUNK - 1) // CHUNK)
print(size, chunks)
# 청크 수: 차례대로 1, 1, 1, 2
완료 기준: 각 경우 마지막 표식이 붙을 청크를 표시하고,
마지막 청크가 사라졌을 때 StreamTruncated가 필요한 이유를 설명합니다.
파일 크기 추정에는 청크별 ABYTES와 별도 24바이트 헤더를 구분하세요.
해당 추정 함수는 별도 헤더를 포함하지 않습니다.
읽을 곳: server/pkg/controller/file.go의 getObjectURL(),
server/pkg/repo/file.go의 Create().
완료 기준: DB 롤백이 외부 객체 삭제까지 보장하지 않는다는 설명과, 임시 객체 추적의 필요성을 담은 표를 작성합니다. 응답이 유실된 경우의 정확한 재시도 동작은 추가 코드를 보기 전 단정하지 마세요.
읽을 곳: rust/bindings/frb/photos/src/api/ml_indexing_api.rs와
rust/bindings/napi/photos/src/lib.rs.
indexing::AnalyzeImageRequest로 바뀌는 지점을 표시합니다.완료 기준: 공통 분석 엔진을 수정하는 경우와 바인딩만 수정하는 경우를 각각 한 예씩 제시합니다. 이미지 바이트 복사가 발생하는 지점도 표시하면 데이터 전달 비용까지 공부할 수 있습니다.
하루에 폴더 하나씩 끝내려고 하면 규모에 압도되기 쉽습니다. 주당 4~6시간 정도를 가정한 학습 계획이며, 각 주의 마지막에 설명 그림이나 표 하나를 남깁니다. 처음 접하는 언어는 모든 문법을 끝내기보다 함수 입력·출력·조건문부터 읽어도 됩니다.
| 주차 | 먼저 익힐 개념 | Ente에서 읽을 곳 | 남길 결과물 |
|---|---|---|---|
| 1주 | HTTP 요청·JSON·계정과 권한·암호문 | server/ente/file.go, architecture/README.md | 파일 한 장과 키의 관계도. 서버가 알아야 하는 정보 5개 |
| 2주 | Dart의 Future·Completer·이벤트·예외 정리 | mobile/apps/photos/lib/services/sync/sync_service.dart, mobile/apps/photos/lib/services/sync/remote_sync_service.dart | 중복 요청 시간축과 증분 동기화의 체크포인트 표 |
| 3주 | Go 구조체·메서드·SQL·트랜잭션 | server/pkg/api/file.go → server/pkg/controller/file.go → server/pkg/repo/file.go | 입력 검사표와 부분 실패 지도 |
| 4주 | 키 유도·인증 암호화·스트림 종료·Rust의 Result | rust/crates/core/src/crypto/argon.rs, rust/crates/core/src/crypto/secretbox.rs, rust/crates/core/src/crypto/stream.rs | 암호화 방식 비교와 경계값·잘림 테스트 목록 |
| 5주 | Worker·FFI·데이터 변환·블로킹 작업 | web/packages/base/crypto, Rust의 FRB·N-API 바인딩 | 한 분석 요청이 공통 엔진에 도달하는 두 경로 |
| 6주 | Compose·마이그레이션·CI·복구·릴리스 | server/compose.yaml, .github/workflows/rust-test.yml, CLI 이전 문서 | 필수 운영 데이터 목록과 개발 버전·릴리스 버전 구분표 |
각 주에 새로운 기능을 하나 더 읽기보다 지난주 그림의 틀린 화살표를 고치는 데 시간을 쓰세요. 폴더 이름을 기억하는 것보다, 입력과 결과가 어느 경계에서 바뀌는지 설명하는 데 집중합니다.
이 저장소를 다 읽는 것보다, 한 흐름을 다른 사람에게 소스 근거와 함께 설명할 수 있는 상태가 더 좋은 학습 목표입니다. 예를 들어 “새 사진이 다른 기기에 나타나기까지”를 입력·암호화·저장·동기화·복호화로 나눠 설명해 보세요. 설명이 막히는 경계가 다음에 읽을 곳입니다.
“증분 동기화 → _syncCollectionDiff() → 체크포인트 갱신 전후 실패”처럼 적어 보세요.
개념만 쓰면 추상적이고 함수만 쓰면 위치 기록에 그칩니다. 실패 조건을 더하면 설계 의도가 드러납니다.
| 용어 | 쉬운 설명 |
|---|---|
| 모노레포 | 여러 앱과 공통 부품을 한 저장소에서 관리하는 방식. 제품과 계층이 함께 나뉩니다. |
| 클라이언트 | 사용자 대신 서버와 통신하는 앱. Ente에서는 복호화와 일부 분석도 수행합니다. |
| Museum | Ente API 서버의 이름. 계정·권한·용량·데이터 위치와 복제 등의 업무를 맡습니다. |
| E2EE | 사용자의 기기에서 암호화하고 허가받은 기기에서 복호화하는 구조입니다. |
| HTTPS / TLS | 통신 구간을 보호하는 기술. 서버가 저장 내용을 읽을 수 있는지와는 별도 문제입니다. |
| 평문 / 암호문 | 읽을 수 있는 원래 데이터와 키 없이 내용을 알아보기 어렵게 변환한 데이터입니다. |
| KEK | 키를 잠그는 키. 이 문서에서는 비밀번호로부터 유도해 masterKey를 보호하는 키입니다. |
| masterKey | 개인 키 계층의 중심인 임의 키. 비밀번호 자체와 같지 않고 암호화된 형태로 보관됩니다. |
| collectionKey / fileKey | 앨범 등에 해당하는 묶음의 키와 개별 파일의 키. 파일 키를 컬렉션 키로 감쌉니다. |
| salt | 키 유도 입력에 넣는 임의 값. 같은 비밀번호라도 다른 유도 결과를 만들도록 돕습니다. |
| nonce | 암호화할 때 사용하는 별도 값. 비밀키와 다르며 해당 알고리즘의 사용 규칙을 지켜야 합니다. |
| Argon2id | 메모리와 계산을 소비하도록 설계된 비밀번호 기반 키 유도 방식입니다. |
| 인증 암호화 | 내용을 숨기는 동시에 변조 여부도 검증하는 암호화 방식입니다. |
| SRP | 비밀번호 원문을 그대로 보내지 않고 알고 있음을 증명하는 인증 프로토콜 계열. Ente의 구현 세부도 확인해야 합니다. |
| 2FA / TOTP | 추가 인증 수단과 시간 기반 일회용 코드. 데이터 복호화용 파일 키와 역할이 다릅니다. |
| 서명된 URL | 서버가 정한 조건과 유효기간 안에서 객체를 읽거나 쓰게 하는 URL. 파일 복호화 키는 아닙니다. |
| 객체 저장소 | 파일을 경로처럼 생긴 키로 보관하는 서비스. 이 저장소는 S3 호환 API를 사용합니다. |
| repository | DB에서 데이터를 읽고 쓰는 책임을 모은 코드. Git 저장소라는 의미와 문맥을 구분합니다. |
| 트랜잭션 | DB 변경을 함께 확정하거나 취소하는 단위. 외부 S3 작업까지 자동으로 포함되지는 않습니다. |
| 증분 동기화 / diff | 전체 대신 지난 확인 이후 바뀐 부분을 받는 방식과 그 변경 내역입니다. |
| 체크포인트 | 어디까지 변경을 반영했는지 기억하는 위치나 시각입니다. |
| single-flight | 같은 작업이 진행 중일 때 그 결과를 공유해 실행이 겹치지 않도록 하는 패턴입니다. |
| Future / Completer | 나중에 얻을 결과와 그 결과를 직접 완료하는 도구. Dart 동기화 코드의 핵심입니다. |
| Worker / isolate | 주 화면 흐름과 작업을 분리하는 실행 단위. 언어와 런타임에 따라 메모리·통신 규칙이 다릅니다. |
| FFI / 바인딩 | 다른 언어의 기능을 부르는 경계와 연결 코드. 입력·오류·결과 변환이 필요합니다. |
| WASM | 브라우저 등에서 실행할 수 있는 이식 가능한 코드 형식. 이 저장소는 일부 Rust 기능을 여기에 연결합니다. |
| 임베딩 | 이미지나 문장을 특징을 나타내는 숫자 벡터로 바꾼 결과. 검색·비교에 사용합니다. |
| ONNX Runtime | ONNX 형식 모델을 실행하는 엔진. 모델 파일 자체와는 다른 구성 요소입니다. |
| 마이그레이션 | DB 스키마나 도구 사용 방식을 이전 상태에서 새 상태로 옮기는 절차입니다. |
| feature | Rust에서 기능과 의존성 조합을 선택하는 장치. 앱 UI의 기능 버튼과는 문맥이 다릅니다. |
| 항목 | 확인 내용 |
|---|---|
| 선정 | GitHub Trending Dart Daily #1 · 2026-10-23 |
| 스타 / 포크 | 29,541 / 1,874 · 2026-10-23 GitHub 기준 |
| 주 언어 / 생성일 | Dart / 2022-11-01 |
| 설명 | 💚 End-to-end encrypted cloud for everything. · 2026-10-23 GitHub 기준 |
| 최신 릴리스 | ente-cli-v0.4.0 · 2026-10-23 |
| 최근 push | 2026-10-23 · GitHub 기준 |
| 최근 커밋 | 939ee85 · 2026-10-23 · Ente CLI 가이드 공개 문서 변경, PR #13985 |
| 개발 중 CLI 버전 | rust/apps/cli/Cargo.toml: 0.4.1. 최신 릴리스와 구별 |
| 코드 규모 | 주요 10개 언어·SQL 합계 약 984,236줄. Git 추적 일반 파일은 전체 8,240개 |
| 라이선스 | 루트 LICENSE: AGPL-3.0 (GNU AGPL v3). 개별 의존성·모델의 라이선스는 별도 확인 |
커밋 939ee85 기준 Git 추적 일반 파일의 확장자별 물리적 줄 수입니다.
빈 줄·주석·테스트·추적된 생성 코드를 포함하며 Git 내부 데이터·비추적 파일·바이너리 자산은 아래 언어 집계에서 제외합니다.
직접 작성한 구현만의 줄 수나 기능의 복잡도 순위로 읽으면 안 됩니다.
| 언어·분류 | 파일 수 | 대략적인 물리적 줄 수 |
|---|---|---|
| Dart | 1,906 | 399,336 |
| TypeScript | 1,136 | 246,324 · .ts와 .tsx 합계 |
| Rust | 499 | 168,836 |
| Go | 553 | 95,286 · 서버 89,070, 기존 CLI 6,216 |
| Kotlin | 184 | 27,395 |
| Swift | 165 | 26,825 |
| JavaScript | 122 | 8,520 · .js/.jsx/.mjs/.cjs 합계 |
| Python | 30 | 7,148 |
| SQL | 306 | 3,600 |
| C/C++ | 25 | 966 · .c/.cpp/.h/.hpp 합계 |
대부분의 Dart는 mobile/에 있고, TypeScript 중 236,783줄은 web/에 있습니다.
Rust에는 생성 바인딩도 포함됩니다. 집계는 확장자 기반이므로 HTML·CSS·문서·설정·번역 자산 전체의 합계와는 다릅니다.
누적 커밋 수와 기여자별 기여량을 산정하려면 전체 Git 이력이 필요합니다.
보안: 루트 README는 Cure53·Symbolic Software·Fallible의 외부 감사를 소개합니다. 이 사실이 커밋의 모든 파일이나 이후 모든 변경까지 검증됐다는 뜻은 아닙니다. 정적 코드 분석은 보안 감사나 성능 시험을 대신하지 않습니다.
인증: architecture/README.md의 이메일·암호화 토큰 중심 설명만으로 전체 로그인을 설명할 수 없습니다.
rust/crates/accounts/src/login.rs의 LoginFlow는 이메일 코드·비밀번호·추가 인증 상태를 다루며,
rust/crates/accounts/src/auth/srp.rs는 SrpSession::compute_m1()과 verify_m2()를 구현합니다.
가용성: README의 3중 데이터 복제는 서비스의 설명입니다. 로컬 Compose의 세 버킷만으로 세 사업자·세 장애 영역의 독립성이 생기지는 않습니다. 실제 배포의 내구성과 장애 복구 시간은 로컬 구성 예제만으로 알 수 없습니다.
현재성: 스타·릴리스·트렌딩은 분석일의 시점 정보이고 온라인 도움말은 바뀔 수 있습니다. 현재 소스의 존재만으로 모든 개발 중 앱·feature가 동일하게 출시됐다고 해석하지 마세요. 성능 수치는 별도 측정이 필요하며, 전체 기여 이력과 경쟁 제품의 정책은 각각 전체 Git 이력과 해당 제품의 공식 자료를 기준으로 판단해야 합니다.
가장 오래 남길 배움은 암호화·저장·동기화의 경계마다 지켜야 할 조건을 찾아내는 읽기 습관입니다. 그 조건을 함수 이름과 함께 설명할 수 있다면, 다른 클라우드 앱을 볼 때도 같은 질문을 던질 수 있습니다.