homebridge-git-backup 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/config.schema.json +70 -0
- package/dist/git-backup.service.d.ts +21 -0
- package/dist/git-backup.service.js +147 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/interfaces.d.ts +11 -0
- package/dist/interfaces.js +2 -0
- package/dist/platform.d.ts +17 -0
- package/dist/platform.js +153 -0
- package/dist/settings.d.ts +2 -0
- package/dist/settings.js +5 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Charles Stephen Thompson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# homebridge-git-backup
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/homebridge-git-backup)
|
|
4
|
+
[](https://homebridge.io)
|
|
5
|
+
|
|
6
|
+
Homebridge plugin that backs up your Homebridge `config.json` to **any Git remote** — GitHub, Forgejo, GitLab, Bitbucket, or a self-hosted git server — every time the config changes and on a schedule.
|
|
7
|
+
|
|
8
|
+
Built on [isomorphic-git](https://isomorphic-git.org), so it works with no `git` binary on the host. That matters for the official Homebridge Docker image, which doesn't include `git`.
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
The previous generation of "backup to GitHub" Homebridge plugins called the GitHub REST API directly. That meant:
|
|
13
|
+
|
|
14
|
+
- They only worked with GitHub.
|
|
15
|
+
- They wrote one file at a time via REST (no atomic multi-file backups).
|
|
16
|
+
- The "backup" lost the commit graph — every backup was a flat file overwrite.
|
|
17
|
+
|
|
18
|
+
This plugin does it the right way: it maintains a real Git working tree under Homebridge's storage directory, fast-forwards from the remote before each backup, commits the new config, and pushes via HTTPS. You get a full commit history of every Homebridge config change.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
From the Homebridge UI: **Plugins → search "Git Backup" → Install**.
|
|
23
|
+
|
|
24
|
+
Or from the command line:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g homebridge-git-backup
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Configure
|
|
31
|
+
|
|
32
|
+
Add a platform block to your Homebridge `config.json` (or use the Homebridge UI form):
|
|
33
|
+
|
|
34
|
+
```jsonc
|
|
35
|
+
{
|
|
36
|
+
"platforms": [
|
|
37
|
+
{
|
|
38
|
+
"platform": "GitBackup",
|
|
39
|
+
"name": "Git Backup",
|
|
40
|
+
"repository_url": "https://github.com/you/homebridge-backups.git",
|
|
41
|
+
"branch": "main",
|
|
42
|
+
"git_username": "git",
|
|
43
|
+
"git_token": "ghp_yourPersonalAccessToken",
|
|
44
|
+
"file_path": "homebridge-config.json",
|
|
45
|
+
"backup_interval": 1440,
|
|
46
|
+
"commit_name": "Homebridge Git Backup",
|
|
47
|
+
"commit_email": "homebridge@localhost"
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Required fields
|
|
54
|
+
|
|
55
|
+
| Field | Description |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `repository_url` | Full HTTPS clone URL of the backup repository |
|
|
58
|
+
| `git_token` | Personal Access Token, deploy key, or password for HTTPS auth |
|
|
59
|
+
|
|
60
|
+
### Provider-specific token setup
|
|
61
|
+
|
|
62
|
+
| Provider | `git_username` | `git_token` |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| GitHub | `git` (any string works) | [Personal Access Token (classic)](https://github.com/settings/tokens) with `repo` scope, or a fine-grained PAT with "Contents: Read and write" |
|
|
65
|
+
| Forgejo / Gitea | your Forgejo username | Access token from **Settings → Applications** with `write:repository` |
|
|
66
|
+
| GitLab | `oauth2` | Personal Access Token with `write_repository` scope |
|
|
67
|
+
| Bitbucket | your username | App password with "Repositories: Write" |
|
|
68
|
+
| Self-hosted (HTTP basic auth) | username | password |
|
|
69
|
+
|
|
70
|
+
> **Security tip:** create a backup-only repository and a token whose scope is restricted to that single repository. Don't reuse a token that has access to other repos.
|
|
71
|
+
|
|
72
|
+
### Optional fields
|
|
73
|
+
|
|
74
|
+
| Field | Default | Description |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| `branch` | `main` | Branch to push backups to |
|
|
77
|
+
| `file_path` | `homebridge-config.json` | Path within the repository where the config is stored. Subdirectories are created automatically. |
|
|
78
|
+
| `backup_interval` | `1440` (24h) | Scheduled backup cadence in minutes (minimum 5) |
|
|
79
|
+
| `commit_name` | `Homebridge Git Backup` | Author name on backup commits |
|
|
80
|
+
| `commit_email` | `homebridge@localhost` | Author email on backup commits |
|
|
81
|
+
|
|
82
|
+
## How it works
|
|
83
|
+
|
|
84
|
+
1. **On startup**, the plugin clones the target repository (shallow, single branch) into `<homebridge-storage>/git-backup-workdir`. If the repository is empty or unreachable, it falls back to initializing a new local repo for the first push.
|
|
85
|
+
2. **On every Homebridge `config.json` change** (debounced 5s) and **on a schedule**, the plugin:
|
|
86
|
+
- Fast-forwards the local repo from the remote (so concurrent backups from other Homebridge instances don't deadlock the branch).
|
|
87
|
+
- Copies the current `config.json` to the configured `file_path`.
|
|
88
|
+
- Stages, commits, and pushes via HTTPS using the configured token.
|
|
89
|
+
- If nothing changed, no commit is made.
|
|
90
|
+
3. **On shutdown**, watchers and timers are released cleanly.
|
|
91
|
+
|
|
92
|
+
Backup runs are serialized — if a change arrives while a backup is in flight, it's coalesced and run once the current one finishes.
|
|
93
|
+
|
|
94
|
+
## Running locally
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
git clone https://github.com/charlestephen/homebridge-git-backup.git
|
|
98
|
+
cd homebridge-git-backup
|
|
99
|
+
npm install
|
|
100
|
+
npm run build
|
|
101
|
+
npm link
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Then in your Homebridge install: `npm link homebridge-git-backup` and restart Homebridge.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pluginAlias": "GitBackup",
|
|
3
|
+
"pluginType": "platform",
|
|
4
|
+
"singular": true,
|
|
5
|
+
"headerDisplay": "Backs up your Homebridge configuration to any Git remote (GitHub, Forgejo, GitLab, self-hosted) on every change and on a schedule.",
|
|
6
|
+
"footerDisplay": "Powered by [isomorphic-git](https://isomorphic-git.org) - no `git` binary required on the host.",
|
|
7
|
+
"schema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"name": {
|
|
11
|
+
"title": "Name",
|
|
12
|
+
"type": "string",
|
|
13
|
+
"default": "Git Backup",
|
|
14
|
+
"required": true
|
|
15
|
+
},
|
|
16
|
+
"repository_url": {
|
|
17
|
+
"title": "Repository URL",
|
|
18
|
+
"type": "string",
|
|
19
|
+
"format": "uri",
|
|
20
|
+
"required": true,
|
|
21
|
+
"description": "Full HTTPS clone URL, e.g. https://github.com/you/homebridge-backups.git",
|
|
22
|
+
"placeholder": "https://github.com/you/homebridge-backups.git"
|
|
23
|
+
},
|
|
24
|
+
"branch": {
|
|
25
|
+
"title": "Branch",
|
|
26
|
+
"type": "string",
|
|
27
|
+
"default": "main",
|
|
28
|
+
"description": "Branch to push backups to. The plugin will fast-forward from this branch before each commit."
|
|
29
|
+
},
|
|
30
|
+
"git_username": {
|
|
31
|
+
"title": "Git Username",
|
|
32
|
+
"type": "string",
|
|
33
|
+
"default": "git",
|
|
34
|
+
"description": "Most token-based providers accept any value here (e.g. 'git' for GitHub PATs; the token-owner username for Forgejo/Gitea)."
|
|
35
|
+
},
|
|
36
|
+
"git_token": {
|
|
37
|
+
"title": "Git Token / Password",
|
|
38
|
+
"type": "string",
|
|
39
|
+
"required": true,
|
|
40
|
+
"format": "password",
|
|
41
|
+
"description": "Personal Access Token, deploy key, or password used for HTTPS authentication. Needs write access to the target repository.",
|
|
42
|
+
"minLength": 1
|
|
43
|
+
},
|
|
44
|
+
"file_path": {
|
|
45
|
+
"title": "File path in repository",
|
|
46
|
+
"type": "string",
|
|
47
|
+
"default": "homebridge-config.json",
|
|
48
|
+
"description": "Path within the repository where the Homebridge config.json should be written. Subdirectories are created automatically."
|
|
49
|
+
},
|
|
50
|
+
"backup_interval": {
|
|
51
|
+
"title": "Backup interval (minutes)",
|
|
52
|
+
"type": "integer",
|
|
53
|
+
"default": 1440,
|
|
54
|
+
"minimum": 5,
|
|
55
|
+
"description": "Scheduled backup cadence. The plugin also commits immediately when the Homebridge config changes on disk."
|
|
56
|
+
},
|
|
57
|
+
"commit_name": {
|
|
58
|
+
"title": "Commit author name",
|
|
59
|
+
"type": "string",
|
|
60
|
+
"default": "Homebridge Git Backup"
|
|
61
|
+
},
|
|
62
|
+
"commit_email": {
|
|
63
|
+
"title": "Commit author email",
|
|
64
|
+
"type": "string",
|
|
65
|
+
"format": "email",
|
|
66
|
+
"default": "homebridge@localhost"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Logging } from 'homebridge';
|
|
2
|
+
export interface GitBackupOptions {
|
|
3
|
+
repositoryUrl: string;
|
|
4
|
+
branch: string;
|
|
5
|
+
username: string;
|
|
6
|
+
token: string;
|
|
7
|
+
filePath: string;
|
|
8
|
+
commitName: string;
|
|
9
|
+
commitEmail: string;
|
|
10
|
+
workDir: string;
|
|
11
|
+
log: Logging;
|
|
12
|
+
}
|
|
13
|
+
export declare class GitBackupService {
|
|
14
|
+
private readonly opts;
|
|
15
|
+
private initialized;
|
|
16
|
+
constructor(opts: GitBackupOptions);
|
|
17
|
+
backup(sourceConfigPath: string): Promise<void>;
|
|
18
|
+
private ensureRepo;
|
|
19
|
+
private fastForward;
|
|
20
|
+
private exists;
|
|
21
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.GitBackupService = void 0;
|
|
40
|
+
const node_fs_1 = require("node:fs");
|
|
41
|
+
const fs = __importStar(require("node:fs"));
|
|
42
|
+
const path = __importStar(require("node:path"));
|
|
43
|
+
const isomorphic_git_1 = __importDefault(require("isomorphic-git"));
|
|
44
|
+
const node_1 = __importDefault(require("isomorphic-git/http/node"));
|
|
45
|
+
class GitBackupService {
|
|
46
|
+
opts;
|
|
47
|
+
initialized = false;
|
|
48
|
+
constructor(opts) {
|
|
49
|
+
this.opts = opts;
|
|
50
|
+
}
|
|
51
|
+
async backup(sourceConfigPath) {
|
|
52
|
+
await this.ensureRepo();
|
|
53
|
+
await this.fastForward();
|
|
54
|
+
const destAbsolute = path.join(this.opts.workDir, this.opts.filePath);
|
|
55
|
+
await node_fs_1.promises.mkdir(path.dirname(destAbsolute), { recursive: true });
|
|
56
|
+
await node_fs_1.promises.copyFile(sourceConfigPath, destAbsolute);
|
|
57
|
+
const status = await isomorphic_git_1.default.statusMatrix({
|
|
58
|
+
fs,
|
|
59
|
+
dir: this.opts.workDir,
|
|
60
|
+
filepaths: [this.opts.filePath],
|
|
61
|
+
});
|
|
62
|
+
const hasChanges = status.some(([, head, workdir, stage]) => head !== workdir || workdir !== stage);
|
|
63
|
+
if (!hasChanges) {
|
|
64
|
+
this.opts.log.debug('Homebridge config unchanged — nothing to commit.');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
await isomorphic_git_1.default.add({ fs, dir: this.opts.workDir, filepath: this.opts.filePath });
|
|
68
|
+
const sha = await isomorphic_git_1.default.commit({
|
|
69
|
+
fs,
|
|
70
|
+
dir: this.opts.workDir,
|
|
71
|
+
message: `Backup Homebridge config: ${new Date().toISOString()}`,
|
|
72
|
+
author: { name: this.opts.commitName, email: this.opts.commitEmail },
|
|
73
|
+
});
|
|
74
|
+
this.opts.log.info(`Committed backup ${sha.slice(0, 7)} on ${this.opts.branch}.`);
|
|
75
|
+
await isomorphic_git_1.default.push({
|
|
76
|
+
fs,
|
|
77
|
+
http: node_1.default,
|
|
78
|
+
dir: this.opts.workDir,
|
|
79
|
+
remote: 'origin',
|
|
80
|
+
ref: this.opts.branch,
|
|
81
|
+
onAuth: () => ({ username: this.opts.username, password: this.opts.token }),
|
|
82
|
+
});
|
|
83
|
+
this.opts.log.info(`Pushed ${sha.slice(0, 7)} to ${this.opts.repositoryUrl}.`);
|
|
84
|
+
}
|
|
85
|
+
async ensureRepo() {
|
|
86
|
+
if (this.initialized)
|
|
87
|
+
return;
|
|
88
|
+
const gitDir = path.join(this.opts.workDir, '.git');
|
|
89
|
+
const exists = await this.exists(gitDir);
|
|
90
|
+
if (!exists) {
|
|
91
|
+
await node_fs_1.promises.mkdir(this.opts.workDir, { recursive: true });
|
|
92
|
+
this.opts.log.info(`Cloning ${this.opts.repositoryUrl} (branch ${this.opts.branch})...`);
|
|
93
|
+
try {
|
|
94
|
+
await isomorphic_git_1.default.clone({
|
|
95
|
+
fs,
|
|
96
|
+
http: node_1.default,
|
|
97
|
+
dir: this.opts.workDir,
|
|
98
|
+
url: this.opts.repositoryUrl,
|
|
99
|
+
ref: this.opts.branch,
|
|
100
|
+
singleBranch: true,
|
|
101
|
+
depth: 1,
|
|
102
|
+
onAuth: () => ({ username: this.opts.username, password: this.opts.token }),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
107
|
+
this.opts.log.warn(`Clone failed (${msg}). Initializing empty repo for first push.`);
|
|
108
|
+
await isomorphic_git_1.default.init({ fs, dir: this.opts.workDir, defaultBranch: this.opts.branch });
|
|
109
|
+
await isomorphic_git_1.default.addRemote({
|
|
110
|
+
fs,
|
|
111
|
+
dir: this.opts.workDir,
|
|
112
|
+
remote: 'origin',
|
|
113
|
+
url: this.opts.repositoryUrl,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
this.initialized = true;
|
|
118
|
+
}
|
|
119
|
+
async fastForward() {
|
|
120
|
+
try {
|
|
121
|
+
await isomorphic_git_1.default.pull({
|
|
122
|
+
fs,
|
|
123
|
+
http: node_1.default,
|
|
124
|
+
dir: this.opts.workDir,
|
|
125
|
+
ref: this.opts.branch,
|
|
126
|
+
singleBranch: true,
|
|
127
|
+
fastForwardOnly: true,
|
|
128
|
+
author: { name: this.opts.commitName, email: this.opts.commitEmail },
|
|
129
|
+
onAuth: () => ({ username: this.opts.username, password: this.opts.token }),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
134
|
+
this.opts.log.debug(`Fast-forward skipped: ${msg}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async exists(p) {
|
|
138
|
+
try {
|
|
139
|
+
await node_fs_1.promises.access(p);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
exports.GitBackupService = GitBackupService;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const platform_1 = require("./platform");
|
|
4
|
+
const settings_1 = require("./settings");
|
|
5
|
+
exports.default = (api) => {
|
|
6
|
+
api.registerPlatform(settings_1.PLATFORM_NAME, platform_1.GitBackupPlatform);
|
|
7
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PlatformConfig } from 'homebridge';
|
|
2
|
+
export interface GitBackupConfig extends PlatformConfig {
|
|
3
|
+
repository_url: string;
|
|
4
|
+
branch?: string;
|
|
5
|
+
git_username?: string;
|
|
6
|
+
git_token: string;
|
|
7
|
+
file_path?: string;
|
|
8
|
+
backup_interval?: number;
|
|
9
|
+
commit_name?: string;
|
|
10
|
+
commit_email?: string;
|
|
11
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { API, DynamicPlatformPlugin, Logging, PlatformAccessory, PlatformConfig } from 'homebridge';
|
|
2
|
+
export declare class GitBackupPlatform implements DynamicPlatformPlugin {
|
|
3
|
+
readonly log: Logging;
|
|
4
|
+
readonly api: API;
|
|
5
|
+
private readonly cfg;
|
|
6
|
+
private service?;
|
|
7
|
+
private watcher?;
|
|
8
|
+
private intervalTimer?;
|
|
9
|
+
private inFlight;
|
|
10
|
+
private pending;
|
|
11
|
+
constructor(log: Logging, config: PlatformConfig, api: API);
|
|
12
|
+
configureAccessory(_accessory: PlatformAccessory): void;
|
|
13
|
+
private validate;
|
|
14
|
+
private start;
|
|
15
|
+
private runBackup;
|
|
16
|
+
private stop;
|
|
17
|
+
}
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.GitBackupPlatform = void 0;
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const chokidar_1 = __importDefault(require("chokidar"));
|
|
42
|
+
const git_backup_service_1 = require("./git-backup.service");
|
|
43
|
+
const settings_1 = require("./settings");
|
|
44
|
+
const DEFAULT_INTERVAL_MINUTES = 1440;
|
|
45
|
+
const MINIMUM_INTERVAL_MINUTES = 5;
|
|
46
|
+
const WATCHER_DEBOUNCE_MS = 5000;
|
|
47
|
+
class GitBackupPlatform {
|
|
48
|
+
log;
|
|
49
|
+
api;
|
|
50
|
+
cfg;
|
|
51
|
+
service;
|
|
52
|
+
watcher;
|
|
53
|
+
intervalTimer;
|
|
54
|
+
inFlight = false;
|
|
55
|
+
pending = false;
|
|
56
|
+
constructor(log, config, api) {
|
|
57
|
+
this.log = log;
|
|
58
|
+
this.api = api;
|
|
59
|
+
this.cfg = config;
|
|
60
|
+
if (!this.validate())
|
|
61
|
+
return;
|
|
62
|
+
this.api.on("didFinishLaunching" /* APIEvent.DID_FINISH_LAUNCHING */, () => this.start());
|
|
63
|
+
this.api.on("shutdown" /* APIEvent.SHUTDOWN */, () => this.stop());
|
|
64
|
+
}
|
|
65
|
+
configureAccessory(_accessory) {
|
|
66
|
+
// No accessories - this is a service-only platform plugin.
|
|
67
|
+
}
|
|
68
|
+
validate() {
|
|
69
|
+
if (!this.cfg) {
|
|
70
|
+
this.log.error(`${settings_1.PLATFORM_NAME}: missing platform config block.`);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (!this.cfg.repository_url) {
|
|
74
|
+
this.log.error(`${settings_1.PLATFORM_NAME}: "repository_url" is required.`);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (!this.cfg.git_token) {
|
|
78
|
+
this.log.error(`${settings_1.PLATFORM_NAME}: "git_token" is required.`);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
start() {
|
|
84
|
+
const branch = this.cfg.branch ?? 'main';
|
|
85
|
+
const username = this.cfg.git_username ?? 'git';
|
|
86
|
+
const filePath = this.cfg.file_path ?? 'homebridge-config.json';
|
|
87
|
+
const commitName = this.cfg.commit_name ?? 'Homebridge Git Backup';
|
|
88
|
+
const commitEmail = this.cfg.commit_email ?? 'homebridge@localhost';
|
|
89
|
+
const intervalMinutes = Math.max(MINIMUM_INTERVAL_MINUTES, this.cfg.backup_interval ?? DEFAULT_INTERVAL_MINUTES);
|
|
90
|
+
const workDir = path.join(this.api.user.storagePath(), 'git-backup-workdir');
|
|
91
|
+
this.service = new git_backup_service_1.GitBackupService({
|
|
92
|
+
repositoryUrl: this.cfg.repository_url,
|
|
93
|
+
branch,
|
|
94
|
+
username,
|
|
95
|
+
token: this.cfg.git_token,
|
|
96
|
+
filePath,
|
|
97
|
+
commitName,
|
|
98
|
+
commitEmail,
|
|
99
|
+
workDir,
|
|
100
|
+
log: this.log,
|
|
101
|
+
});
|
|
102
|
+
void this.runBackup('startup');
|
|
103
|
+
this.watcher = chokidar_1.default.watch(this.api.user.configPath(), {
|
|
104
|
+
persistent: true,
|
|
105
|
+
ignoreInitial: true,
|
|
106
|
+
awaitWriteFinish: {
|
|
107
|
+
stabilityThreshold: WATCHER_DEBOUNCE_MS,
|
|
108
|
+
pollInterval: 100,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
this.watcher.on('change', () => void this.runBackup('config change'));
|
|
112
|
+
this.intervalTimer = setInterval(() => void this.runBackup('scheduled'), intervalMinutes * 60 * 1000);
|
|
113
|
+
this.log.info(`${settings_1.PLATFORM_NAME} watching ${this.api.user.configPath()}; ` +
|
|
114
|
+
`scheduled every ${intervalMinutes} minute(s).`);
|
|
115
|
+
}
|
|
116
|
+
async runBackup(trigger) {
|
|
117
|
+
if (!this.service)
|
|
118
|
+
return;
|
|
119
|
+
if (this.inFlight) {
|
|
120
|
+
this.pending = true;
|
|
121
|
+
this.log.debug(`Backup already running; coalescing trigger "${trigger}".`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
this.inFlight = true;
|
|
125
|
+
try {
|
|
126
|
+
this.log.debug(`Running backup (trigger: ${trigger}).`);
|
|
127
|
+
await this.service.backup(this.api.user.configPath());
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
131
|
+
this.log.error(`Backup failed (${trigger}): ${msg}`);
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
this.inFlight = false;
|
|
135
|
+
if (this.pending) {
|
|
136
|
+
this.pending = false;
|
|
137
|
+
void this.runBackup('coalesced');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
stop() {
|
|
142
|
+
this.log.info(`${settings_1.PLATFORM_NAME} shutting down.`);
|
|
143
|
+
if (this.intervalTimer) {
|
|
144
|
+
clearInterval(this.intervalTimer);
|
|
145
|
+
this.intervalTimer = undefined;
|
|
146
|
+
}
|
|
147
|
+
if (this.watcher) {
|
|
148
|
+
void this.watcher.close();
|
|
149
|
+
this.watcher = undefined;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
exports.GitBackupPlatform = GitBackupPlatform;
|
package/dist/settings.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "homebridge-git-backup",
|
|
3
|
+
"displayName": "Git Backup",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Homebridge plugin that backs up your Homebridge configuration to any Git remote (GitHub, Forgejo, GitLab, self-hosted) on every change.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "charlestephen",
|
|
8
|
+
"type": "commonjs",
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"config.schema.json",
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": "^22 || ^24",
|
|
18
|
+
"homebridge": "^2.0.0"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"clean": "rm -rf dist",
|
|
22
|
+
"build": "npm run clean && tsc",
|
|
23
|
+
"watch": "tsc --watch",
|
|
24
|
+
"prepublishOnly": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"homebridge-plugin",
|
|
28
|
+
"homebridge",
|
|
29
|
+
"backup",
|
|
30
|
+
"git",
|
|
31
|
+
"github",
|
|
32
|
+
"forgejo",
|
|
33
|
+
"gitlab",
|
|
34
|
+
"config",
|
|
35
|
+
"isomorphic-git"
|
|
36
|
+
],
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"chokidar": "^5.0.0",
|
|
39
|
+
"isomorphic-git": "^1.38.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"homebridge": "^2.0.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^22.10.0",
|
|
46
|
+
"typescript": "^5.7.0"
|
|
47
|
+
},
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/charlestephen/homebridge-git-backup.git"
|
|
51
|
+
},
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/charlestephen/homebridge-git-backup/issues"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/charlestephen/homebridge-git-backup#readme"
|
|
56
|
+
}
|