From 0ed9d7c361b6ca2afd86bd66a2fd1fa0c2a0f387 Mon Sep 17 00:00:00 2001
From: exemchs <185463904+exemchs@users.noreply.github.com>
Date: Mon, 27 Jul 2026 17:21:34 +0900
Subject: [PATCH] Add signed automatic updates
---
Docs/RELEASING.md | 58 ++++++++++++++++++++
Docs/TEAM_DISTRIBUTION.md | 43 ++++++---------
Info.plist | 22 +++++++-
PRIVACY.md | 12 +++--
Package.resolved | 15 ++++++
Package.swift | 17 +++++-
README.md | 54 ++++++++-----------
Scripts/build-app.sh | 10 +++-
Scripts/build-update.sh | 90 ++++++++++++++++++++++++++++++++
Sources/Cue/CueAppDelegate.swift | 7 +++
appcast.xml | 16 ++++++
11 files changed, 278 insertions(+), 66 deletions(-)
create mode 100644 Docs/RELEASING.md
create mode 100644 Package.resolved
create mode 100755 Scripts/build-update.sh
create mode 100644 appcast.xml
diff --git a/Docs/RELEASING.md b/Docs/RELEASING.md
new file mode 100644
index 0000000..e80d5be
--- /dev/null
+++ b/Docs/RELEASING.md
@@ -0,0 +1,58 @@
+# Cue 업데이트 배포
+
+Cue의 자동 업데이트는 두 서명을 함께 사용합니다.
+
+- `Cue Local Signing`: 모든 배포 앱의 코드 서명을 동일하게 유지합니다.
+- Sparkle 계정 `cue`: 내려받는 업데이트 파일이 Cue에서 만든 파일인지
+ EdDSA로 확인합니다.
+
+둘 중 하나를 잃어버리거나 새로 만들면 기존 설치본이 다음 업데이트를
+받지 못할 수 있습니다. 로그인 키체인과 Sparkle 개인 키는 별도의 안전한
+저장소에 백업해 주세요. 개인 키 파일은 저장소에 올리지 않습니다.
+
+```sh
+.build/artifacts/sparkle/Sparkle/bin/generate_keys \
+ --account cue \
+ -x /안전한/외부/경로/cue-sparkle.key
+```
+
+## 새 버전 배포
+
+1. `Info.plist`의 표시 버전과 빌드 번호를 올립니다.
+2. 업데이트 앱과 appcast를 만듭니다.
+
+```sh
+./Scripts/build-update.sh
+```
+
+3. 코드 변경과 새 `appcast.xml`을 `main`에 반영합니다.
+4. 같은 버전의 GitHub Release를 만들고 생성된 ZIP을 올립니다.
+
+```sh
+CUE_RELEASE_VERSION=0.8.7
+gh release create "v$CUE_RELEASE_VERSION" \
+ "dist/Cue-$CUE_RELEASE_VERSION-macos.zip" \
+ --title "Cue $CUE_RELEASE_VERSION" \
+ --notes-file /path/to/release-notes.md
+```
+
+`appcast.xml`의 다운로드 주소와 GitHub Release의 태그·파일명이 정확히
+같아야 합니다. `build-update.sh`는 앱 코드 서명, ZIP 무결성, EdDSA 서명을
+확인한 뒤 파일을 만듭니다.
+
+## 배포 후 확인
+
+```sh
+CUE_RELEASE_VERSION=0.8.7
+curl -fsSL \
+ https://raw.githubusercontent.com/exemchs/cue/main/appcast.xml
+
+curl -I -L \
+ "https://github.com/exemchs/cue/releases/download/v$CUE_RELEASE_VERSION/Cue-$CUE_RELEASE_VERSION-macos.zip"
+```
+
+릴리스 ZIP을 다시 내려받아 압축을 풀고 코드 서명도 확인합니다.
+
+```sh
+codesign --verify --deep --strict /path/to/Cue.app
+```
diff --git a/Docs/TEAM_DISTRIBUTION.md b/Docs/TEAM_DISTRIBUTION.md
index 26a65fd..52f7078 100644
--- a/Docs/TEAM_DISTRIBUTION.md
+++ b/Docs/TEAM_DISTRIBUTION.md
@@ -1,25 +1,15 @@
# 팀원에게 Cue 공유하기
-Cue는 GitHub 저장소를 공유하고, 각자의 Mac에서 직접 빌드해 설치합니다.
-
-한 Mac에서 로컬 서명한 `Cue.app`을 그대로 다른 사람에게 보내면 macOS가
-다른 앱으로 인식하거나 권한을 다시 요청할 수 있습니다. 완성된 앱 파일보다
-저장소 주소와 설치 명령을 전달하는 편이 안정적입니다.
+Cue는 GitHub Release에 올린 동일한 앱을 팀원이 받아 사용합니다. 이후
+버전도 같은 서명으로 배포하기 때문에 전체 디스크 접근 권한을 매번 다시
+허용하지 않아도 됩니다.
## 처음 설치할 때
-```sh
-git clone https://github.com/exemchs/cue.git
-cd cue
-./Scripts/setup-signing-identity.sh
-./Scripts/allow-local-signing.sh
-./Scripts/build-app.sh
-ditto dist/Cue.app /Applications/Cue.app
-open /Applications/Cue.app
-```
-
-`allow-local-signing.sh`는 로그인 키체인 암호를 직접 입력받기 때문에
-Terminal에서 실행해야 합니다. 입력한 암호는 저장하지 않습니다.
+1. [최신 릴리스](https://github.com/exemchs/cue/releases/latest)에서
+ `Cue-버전-macos.zip`을 받습니다.
+2. 압축을 풀고 `Cue.app`을 `/Applications`로 옮깁니다.
+3. 처음 한 번은 `Cue.app`을 Control-클릭하고 **열기**를 선택합니다.
앱을 실행한 뒤에는 다음 항목만 설정합니다.
@@ -33,19 +23,16 @@ Terminal에서 실행해야 합니다. 입력한 암호는 저장하지 않습
Mail 같은 소스를 추가할 때 앱마다 별도 권한을 허용할 필요는 없습니다.
아이콘도 설치된 앱에서 자동으로 가져옵니다.
-## 업데이트할 때
-
-기존 저장소에서 최신 코드를 받은 뒤 같은 명령으로 다시 빌드합니다.
+## 자동 업데이트
-```sh
-git pull
-./Scripts/build-app.sh
-ditto dist/Cue.app /Applications/Cue.app
-open /Applications/Cue.app
-```
+Cue는 6시간마다 GitHub에서 새 버전을 확인합니다. 업데이트 파일은 EdDSA
+서명을 통과한 경우에만 사용하고, Cue를 다음에 종료하거나 다시 실행할 때
+자동으로 설치합니다. Cue를 하루 이상 계속 켜두면 설치 준비가 끝났다는
+안내가 나타날 수 있습니다.
-권한 요청이 반복되지 않도록 처음 만든 로컬 서명 인증서와
-`/Applications/Cue.app` 경로를 계속 사용합니다.
+0.8.6 이하에는 업데이터가 없으므로 최신 버전을 한 번 직접 설치해야 합니다.
+기존에 각자 빌드한 Cue를 사용했다면 공유 빌드로 바뀌는 첫 설치에서만 전체
+디스크 접근 권한을 다시 켜야 할 수 있습니다.
## 회사에서 관리하는 Mac
diff --git a/Info.plist b/Info.plist
index 2fbc048..038c23c 100644
--- a/Info.plist
+++ b/Info.plist
@@ -15,14 +15,32 @@
CFBundlePackageTypeAPPLCFBundleShortVersionString
- 0.8.6
+ 0.8.7CFBundleVersion
- 42
+ 43LSMinimumSystemVersion14.0LSUIElementNSHighResolutionCapable
+ SUAllowsAutomaticUpdates
+
+ SUAutomaticallyUpdate
+
+ SUEnableAutomaticChecks
+
+ SUFeedURL
+ https://raw.githubusercontent.com/exemchs/cue/main/appcast.xml
+ SUPublicEDKey
+ /HhZoRAFWV8E0I80JwexpiZBvqE31OeMGCXwM4BHNmw=
+ SUScheduledCheckInterval
+ 21600
+ SUScheduledImpatientCheckInterval
+ 86400
+ SUShowReleaseNotes
+
+ SUVerifyUpdateBeforeExtraction
+
diff --git a/PRIVACY.md b/PRIVACY.md
index 2cc4088..65638e5 100644
--- a/PRIVACY.md
+++ b/PRIVACY.md
@@ -36,9 +36,15 @@ Mattermost 연결 파일에는 알림을 구분하기 위한 SHA-256 값, 확인
## 네트워크 연결
-Cue는 OpenAI, Mattermost 또는 Cue 서버에 접속하지 않습니다. Mattermost
-연결은 현재 Mac의 `127.0.0.1:49371` 안에서만 동작합니다. Mattermost 앱
-자체는 원래 사용하던 네트워크 연결을 그대로 사용합니다.
+Cue는 OpenAI, Mattermost 또는 별도 Cue 서버에 작업과 알림 데이터를
+보내지 않습니다. Mattermost 연결은 현재 Mac의 `127.0.0.1:49371`
+안에서만 동작합니다. Mattermost 앱 자체는 원래 사용하던 네트워크 연결을
+그대로 사용합니다.
+
+자동 업데이트를 확인하고 받을 때만 `raw.githubusercontent.com`과
+`github.com`에 연결합니다. 이 요청에는 Cue 버전과 macOS가 일반적인
+네트워크 요청에 포함하는 정보만 전달되며, 작업 제목, 메시지, 소스 설정은
+포함하지 않습니다. 내려받은 앱은 EdDSA 서명을 확인한 뒤 설치합니다.
## 로컬 데이터 지우기
diff --git a/Package.resolved b/Package.resolved
new file mode 100644
index 0000000..1566f0c
--- /dev/null
+++ b/Package.resolved
@@ -0,0 +1,15 @@
+{
+ "originHash" : "1f360802317f2a0908dc908830d9afe475e894286b80960c85cd78772be19ea7",
+ "pins" : [
+ {
+ "identity" : "sparkle",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/sparkle-project/Sparkle",
+ "state" : {
+ "revision" : "6276ba2b404829d139c45ff98427cf90e2efc59b",
+ "version" : "2.9.2"
+ }
+ }
+ ],
+ "version" : 3
+}
diff --git a/Package.swift b/Package.swift
index 7eb72d6..6e9b44a 100644
--- a/Package.swift
+++ b/Package.swift
@@ -10,14 +10,29 @@ let package = Package(
products: [
.executable(name: "Cue", targets: ["Cue"])
],
+ dependencies: [
+ .package(
+ url: "https://github.com/sparkle-project/Sparkle",
+ exact: "2.9.2"
+ )
+ ],
targets: [
.executableTarget(
name: "Cue",
+ dependencies: [
+ .product(name: "Sparkle", package: "Sparkle")
+ ],
resources: [
.process("Resources")
],
linkerSettings: [
- .linkedLibrary("sqlite3")
+ .linkedLibrary("sqlite3"),
+ .unsafeFlags([
+ "-Xlinker",
+ "-rpath",
+ "-Xlinker",
+ "@executable_path/../Frameworks"
+ ])
]
)
]
diff --git a/README.md b/README.md
index c543f71..b1c3309 100644
--- a/README.md
+++ b/README.md
@@ -12,9 +12,9 @@ Cue는 진행 중인 작업과 확인이 필요한 알림을
> [!IMPORTANT]
-> Cue는 아직 직접 빌드해 사용하는 초기 버전입니다. 일부 기능은 공식 API
-> 대신 각 앱이 Mac에 저장한 로컬 데이터를 읽습니다. macOS나 앱이
-> 업데이트되면 일부 연결이 동작하지 않을 수 있습니다. 권한을 허용하기 전
+> Cue의 일부 기능은 공식 API 대신 각 앱이 Mac에 저장한 로컬 데이터를
+> 읽습니다. macOS나 앱이 업데이트되면 일부 연결이 동작하지 않을 수
+> 있습니다. 권한을 허용하기 전
> [개인정보 처리 방식](PRIVACY.md)을 확인해 주세요.
## 왜 만들었나요
@@ -89,39 +89,18 @@ macOS 알림 센터에 저장된 알림 제목과 내용을 보여줍니다. 앱
## 설치
-현재는 각자의 Mac에서 직접 빌드해 설치합니다. Pretendard Light, Regular,
-Medium은 Cue에 포함되어 있어 따로 설치하지 않아도 됩니다.
+1. [최신 릴리스](https://github.com/exemchs/cue/releases/latest)에서
+ `Cue-버전-macos.zip`을 받습니다.
+2. 압축을 풀고 `Cue.app`을 `/Applications`로 옮깁니다.
+3. 처음 한 번은 `Cue.app`을 Control-클릭하고 **열기**를 선택합니다.
### 필요한 환경
- 노치가 있는 MacBook
- macOS 14 이상
-- Swift 6을 포함한 Xcode Command Line Tools
- 상태나 알림을 확인할 ChatGPT, Mattermost 또는 다른 앱
- Homebrew Node.js (Mattermost 알림을 원래 대화로 연결할 때만 필요)
-Command Line Tools가 없다면 먼저 설치합니다.
-
-```sh
-xcode-select --install
-```
-
-Cue를 내려받아 빌드하고 `/Applications`에 설치합니다.
-
-```sh
-git clone https://github.com/exemchs/cue.git
-cd cue
-./Scripts/setup-signing-identity.sh
-./Scripts/allow-local-signing.sh
-./Scripts/build-app.sh
-ditto dist/Cue.app /Applications/Cue.app
-open /Applications/Cue.app
-```
-
-`allow-local-signing.sh`는 로그인 키체인 암호를 직접 입력받기 때문에
-Terminal에서 실행해야 합니다. 입력한 암호는 화면에 표시되거나 저장되지
-않습니다. 만들어진 로컬 인증서도 각 Mac에만 남습니다.
-
설치 후에는 다음 항목만 연결하면 됩니다.
1. 노치의 Cue 영역을 오른쪽 클릭한 뒤 **앱 추가**를 누릅니다.
@@ -130,8 +109,10 @@ Terminal에서 실행해야 합니다. 입력한 암호는 화면에 표시되
보안 → 전체 디스크 접근 권한**에서 `/Applications/Cue.app`을 켭니다.
4. macOS가 요청하는 경우 Cue를 한 번 다시 실행합니다.
-권한을 반복해서 요청하지 않도록 Cue는 `/Applications`에 두고, 업데이트할
-때도 같은 로컬 서명 인증서를 사용합니다.
+Cue는 6시간마다 새 버전을 확인하고, 서명을 확인한 업데이트만 백그라운드로
+받습니다. 받은 업데이트는 Cue를 다음에 종료하거나 다시 실행할 때
+설치됩니다. 0.8.6 이하를 사용 중이라면 위 설치 과정을 한 번만 다시
+진행해야 하며, 그다음부터 자동으로 업데이트됩니다.
## 데이터는 어떻게 처리하나요
@@ -140,6 +121,7 @@ Terminal에서 실행해야 합니다. 입력한 암호는 화면에 표시되
- 분석 도구를 사용하지 않습니다.
- 작업과 알림 데이터를 외부 서버로 보내지 않습니다.
- 설정과 확인 기록은 현재 Mac에만 저장합니다.
+- 새 버전 확인을 위해 GitHub에만 연결합니다.
Cue가 어떤 파일을 읽고 무엇을 저장하는지는
[PRIVACY.md](PRIVACY.md)에 정리해두었습니다.
@@ -165,17 +147,26 @@ swift run Cue --preview-regular-app --preview-many-platforms \
./Scripts/build-app.sh
```
+업데이트 파일과 appcast 만들기(배포 관리자):
+
+```sh
+./Scripts/build-update.sh
+```
+
주요 구성:
- `CueModel`: 소스 설정, 상태 정리, 로컬 저장
- `LocalActivityMonitor`: Codex와 macOS 알림 센터 읽기
- `NotificationDatabaseWatcher`: 새 macOS 알림을 감지해 목록 갱신
- `MattermostBridgeSupervisor`: Mattermost의 원래 대화로 이동하기 위한 로컬 연결
+- `Sparkle`: GitHub 릴리스 확인, 업데이트 검증과 설치
- `CueNotchView`: 노치, 활동 목록, 소스 관리, 순서 변경
- `NotchPanelController`: 노치 위치와 멀티 디스플레이 동작
팀원에게 공유하는 방법은
[팀원 공유 문서](Docs/TEAM_DISTRIBUTION.md)에 정리해두었습니다.
+업데이트를 배포하는 순서는
+[배포 문서](Docs/RELEASING.md)에서 확인할 수 있습니다.
## 함께 만들기
@@ -191,7 +182,8 @@ Cue는 [MIT License](LICENSE)로 배포합니다.
앱에 포함된 Pretendard 1.3.9는 SIL Open Font License 1.1을 따릅니다.
라이선스 원문은
`Sources/Cue/Resources/Licenses/Pretendard-OFL-1.1.txt`에 포함되어
-있습니다.
+있습니다. 자동 업데이트에 사용하는 Sparkle은 MIT License를 따르며,
+배포 앱 안에 라이선스 원문을 함께 포함합니다.
ChatGPT, OpenAI, Mattermost, macOS와 각 앱의 이름 및 아이콘은 해당
권리자에게 속합니다. Cue는 해당 회사의 공식 제품이 아닙니다.
diff --git a/Scripts/build-app.sh b/Scripts/build-app.sh
index a38493e..94e9a45 100755
--- a/Scripts/build-app.sh
+++ b/Scripts/build-app.sh
@@ -48,9 +48,15 @@ swift build -c release
STAGING_DIR="$(mktemp -d "$DIST_DIR/.cue-package.XXXXXX")"
STAGED_APP="$STAGING_DIR/Cue.app"
-mkdir -p "$STAGED_APP/Contents/MacOS" "$STAGED_APP/Contents/Resources"
+mkdir -p \
+ "$STAGED_APP/Contents/MacOS" \
+ "$STAGED_APP/Contents/Resources" \
+ "$STAGED_APP/Contents/Frameworks"
cp "$BUILD_DIR/release/Cue" "$STAGED_APP/Contents/MacOS/Cue"
+ditto "$BUILD_DIR/release/Sparkle.framework" \
+ "$STAGED_APP/Contents/Frameworks/Sparkle.framework"
+
cp "$PROJECT_DIR/Sources/Cue/Resources/"*.mjs \
"$STAGED_APP/Contents/Resources/"
@@ -58,6 +64,8 @@ ditto "$PROJECT_DIR/Sources/Cue/Resources/Fonts" \
"$STAGED_APP/Contents/Resources/Fonts"
ditto "$PROJECT_DIR/Sources/Cue/Resources/Licenses" \
"$STAGED_APP/Contents/Resources/Licenses"
+cp "$BUILD_DIR/checkouts/Sparkle/LICENSE" \
+ "$STAGED_APP/Contents/Resources/Licenses/Sparkle-LICENSE"
cp "$PROJECT_DIR/Info.plist" "$STAGED_APP/Contents/Info.plist"
plutil -replace CFBundleIdentifier \
diff --git a/Scripts/build-update.sh b/Scripts/build-update.sh
new file mode 100755
index 0000000..8d60e75
--- /dev/null
+++ b/Scripts/build-update.sh
@@ -0,0 +1,90 @@
+#!/bin/zsh
+set -euo pipefail
+
+PROJECT_DIR="${0:A:h:h}"
+BUILD_DIR="$PROJECT_DIR/.build"
+DIST_DIR="$PROJECT_DIR/dist"
+APP_DIR="$DIST_DIR/Cue.app"
+SPARKLE_BIN="$BUILD_DIR/artifacts/sparkle/Sparkle/bin"
+SPARKLE_ACCOUNT="${CUE_SPARKLE_ACCOUNT:-cue}"
+STAGING_DIR=""
+
+cleanup() {
+ local exit_code=$?
+ if [[ -n "$STAGING_DIR" && -d "$STAGING_DIR" ]]; then
+ find "$STAGING_DIR" -depth -delete
+ fi
+ return $exit_code
+}
+trap cleanup EXIT
+trap 'exit 130' INT
+trap 'exit 143' TERM
+
+"$PROJECT_DIR/Scripts/build-app.sh"
+
+if [[ ! -x "$SPARKLE_BIN/generate_keys" \
+ || ! -x "$SPARKLE_BIN/generate_appcast" ]]; then
+ print -u2 "Sparkle update tools were not found in $SPARKLE_BIN."
+ exit 1
+fi
+
+VERSION="$(/usr/libexec/PlistBuddy \
+ -c "Print :CFBundleShortVersionString" \
+ "$APP_DIR/Contents/Info.plist")"
+BUILD_NUMBER="$(/usr/libexec/PlistBuddy \
+ -c "Print :CFBundleVersion" \
+ "$APP_DIR/Contents/Info.plist")"
+EMBEDDED_PUBLIC_KEY="$(/usr/libexec/PlistBuddy \
+ -c "Print :SUPublicEDKey" \
+ "$APP_DIR/Contents/Info.plist")"
+KEYCHAIN_PUBLIC_KEY="$("$SPARKLE_BIN/generate_keys" \
+ --account "$SPARKLE_ACCOUNT" \
+ -p)"
+
+if [[ "$EMBEDDED_PUBLIC_KEY" != "$KEYCHAIN_PUBLIC_KEY" ]]; then
+ print -u2 "The Sparkle key in Info.plist does not match account:"
+ print -u2 "$SPARKLE_ACCOUNT"
+ exit 1
+fi
+
+STAGING_DIR="$(mktemp -d "$DIST_DIR/.cue-update.XXXXXX")"
+ARCHIVE_NAME="Cue-$VERSION-macos.zip"
+STAGED_ARCHIVE="$STAGING_DIR/$ARCHIVE_NAME"
+FINAL_ARCHIVE="$DIST_DIR/$ARCHIVE_NAME"
+DOWNLOAD_PREFIX="https://github.com/exemchs/cue/releases/download/v$VERSION/"
+
+ditto -c -k --sequesterRsrc --keepParent \
+ "$APP_DIR" \
+ "$STAGED_ARCHIVE"
+
+"$SPARKLE_BIN/generate_appcast" \
+ --account "$SPARKLE_ACCOUNT" \
+ --download-url-prefix "$DOWNLOAD_PREFIX" \
+ --link "https://github.com/exemchs/cue" \
+ --maximum-versions 1 \
+ --maximum-deltas 0 \
+ -o "$STAGING_DIR/appcast.xml" \
+ "$STAGING_DIR"
+
+UPDATE_SIGNATURE="$(sed -n \
+ 's/.*sparkle:edSignature="\([^"]*\)".*/\1/p' \
+ "$STAGING_DIR/appcast.xml")"
+if [[ -z "$UPDATE_SIGNATURE" ]]; then
+ print -u2 "The generated appcast does not contain an update signature."
+ exit 1
+fi
+"$SPARKLE_BIN/sign_update" \
+ --account "$SPARKLE_ACCOUNT" \
+ --verify \
+ "$STAGED_ARCHIVE" \
+ "$UPDATE_SIGNATURE"
+
+cp "$STAGING_DIR/appcast.xml" "$PROJECT_DIR/appcast.xml"
+mv -f "$STAGED_ARCHIVE" "$FINAL_ARCHIVE"
+
+codesign --verify --deep --strict "$APP_DIR"
+unzip -tq "$FINAL_ARCHIVE" >/dev/null
+
+echo "Cue $VERSION ($BUILD_NUMBER)"
+echo "$FINAL_ARCHIVE"
+echo "$PROJECT_DIR/appcast.xml"
diff --git a/Sources/Cue/CueAppDelegate.swift b/Sources/Cue/CueAppDelegate.swift
index 3847449..b32eae6 100644
--- a/Sources/Cue/CueAppDelegate.swift
+++ b/Sources/Cue/CueAppDelegate.swift
@@ -1,12 +1,19 @@
import AppKit
+import Sparkle
@MainActor
final class CueAppDelegate: NSObject, NSApplicationDelegate {
private var panelController: NotchPanelController?
private var model: CueModel?
+ private lazy var updaterController = SPUStandardUpdaterController(
+ startingUpdater: true,
+ updaterDelegate: nil,
+ userDriverDelegate: nil
+ )
func applicationDidFinishLaunching(_ notification: Notification) {
CueFontRegistry.registerBundledFonts()
+ _ = updaterController
let model = CueModel()
let controller = NotchPanelController(model: model)
diff --git a/appcast.xml b/appcast.xml
new file mode 100644
index 0000000..bf16f83
--- /dev/null
+++ b/appcast.xml
@@ -0,0 +1,16 @@
+
+
+
+ Cue
+
+ 0.8.7
+ Mon, 27 Jul 2026 17:20:20 +0900
+ https://github.com/exemchs/cue
+ 43
+ 0.8.7
+ 14.0
+ arm64
+
+
+
+
\ No newline at end of file