-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhyperbee-http.js
More file actions
201 lines (168 loc) · 6.16 KB
/
Copy pathhyperbee-http.js
File metadata and controls
201 lines (168 loc) · 6.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
import Hypercore from 'hypercore'
import Hyperbee from 'hyperbee'
// pkt-line helpers
function pktLine (str) {
const len = str.length + 4
return len.toString(16).padStart(4, '0') + str
}
function pktLineBuf (buf) {
if (typeof buf === 'string') buf = Buffer.from(buf)
const len = buf.length + 4
const hex = len.toString(16).padStart(4, '0')
return Buffer.concat([Buffer.from(hex), buf])
}
const FLUSH = '0000'
function serviceFromUrl (url) {
const match = url.match(/service=(git-[a-z-]+)/)
return match ? match[1] : null
}
export default async function createHyperbeeHttp (storagePath) {
const core = new Hypercore(storagePath)
const db = new Hyperbee(core, { keyEncoding: 'utf-8', valueEncoding: 'binary' })
await db.ready()
const http = {
async request ({ url, method, headers, body }) {
// GET /info/refs?service=git-receive-pack or git-upload-pack
if (url.includes('/info/refs')) {
const service = serviceFromUrl(url)
let out = ''
out += pktLine(`# service=${service}\n`)
out += FLUSH
// Read existing refs from Hyperbee
const refs = []
for await (const entry of db.createReadStream({ gte: 'ref/', lt: 'ref0' })) {
refs.push({ name: entry.key.slice(4), sha: entry.value.toString() })
}
// Find the default branch to advertise HEAD
const mainRef = refs.find(r => r.name === 'refs/heads/main')
|| refs.find(r => r.name === 'refs/heads/master')
|| refs[0]
const symref = mainRef ? ` symref=HEAD:${mainRef.name}` : ''
const caps = service === 'git-upload-pack'
? `side-band-64k ofs-delta${symref}`
: `report-status delete-refs side-band-64k${symref}`
if (refs.length === 0) {
const zero = '0'.repeat(40)
out += pktLine(zero + ' capabilities^{}\0' + caps + '\n')
} else {
// Advertise HEAD first if we have a default branch
if (mainRef) {
out += pktLine(mainRef.sha + ' HEAD\0' + caps + '\n')
refs.forEach(r => {
out += pktLine(r.sha + ' ' + r.name + '\n')
})
} else {
refs.forEach((r, i) => {
if (i === 0) {
out += pktLine(r.sha + ' ' + r.name + '\0' + caps + '\n')
} else {
out += pktLine(r.sha + ' ' + r.name + '\n')
}
})
}
}
out += FLUSH
const contentType = `application/x-${service}-advertisement`
return {
url,
method,
statusCode: 200,
statusMessage: 'OK',
headers: { 'content-type': contentType },
body: [Buffer.from(out)],
}
}
// POST /git-receive-pack (push)
if (url.includes('/git-receive-pack')) {
const chunks = []
for await (const chunk of body) {
chunks.push(chunk)
}
const data = Buffer.concat(chunks)
// Parse pkt-line commands (old-sha new-sha refname)
let offset = 0
const commands = []
while (offset < data.length) {
const lenHex = data.slice(offset, offset + 4).toString()
const len = parseInt(lenHex, 16)
if (len === 0) {
offset += 4
break
}
const line = data.slice(offset + 4, offset + len).toString().trim()
const clean = line.split('\0')[0]
const parts = clean.split(' ')
if (parts.length >= 3) {
commands.push({ oldSha: parts[0], newSha: parts[1], ref: parts[2] })
}
offset += len
}
// Everything after the flush is the packfile
const packfile = data.slice(offset)
// Store in Hyperbee
const batch = db.batch()
for (const cmd of commands) {
const zero = '0'.repeat(40)
if (cmd.newSha === zero) {
await batch.del('ref/' + cmd.ref)
} else {
await batch.put('ref/' + cmd.ref, Buffer.from(cmd.newSha))
}
}
if (packfile.length > 0) {
await batch.put('pack/' + Date.now(), packfile)
}
await batch.flush()
// Build side-band encoded response
const inner = Buffer.concat([
pktLineBuf('unpack ok\n'),
...commands.map(cmd => pktLineBuf('ok ' + cmd.ref + '\n')),
Buffer.from(FLUSH),
])
const sideBand = Buffer.concat([Buffer.from([0x01]), inner])
const response = Buffer.concat([pktLineBuf(sideBand), Buffer.from(FLUSH)])
return {
url,
method,
statusCode: 200,
statusMessage: 'OK',
headers: { 'content-type': 'application/x-git-receive-pack-result' },
body: [response],
}
}
// POST /git-upload-pack (clone/fetch)
if (url.includes('/git-upload-pack')) {
// Consume the request body (want/have/done lines) — we ignore them
// and just send back all packfiles we have
for await (const chunk of body) { /* drain */ }
// Gather all stored packfiles
const packfiles = []
for await (const entry of db.createReadStream({ gte: 'pack/', lt: 'pack0' })) {
packfiles.push(entry.value)
}
// NAK line (goes to packetlines via demux default case)
const nak = pktLineBuf('NAK\n')
// Packfile data on side-band channel 1
const packData = Buffer.concat(packfiles)
const sideBandChunks = []
// Split packfile into chunks (max ~65000 bytes per side-band pkt-line)
const MAX_CHUNK = 65000
for (let i = 0; i < packData.length; i += MAX_CHUNK) {
const chunk = packData.slice(i, i + MAX_CHUNK)
const sideBand = Buffer.concat([Buffer.from([0x01]), chunk])
sideBandChunks.push(pktLineBuf(sideBand))
}
const response = Buffer.concat([nak, ...sideBandChunks, Buffer.from(FLUSH)])
return {
url,
method,
statusCode: 200,
statusMessage: 'OK',
headers: { 'content-type': 'application/x-git-upload-pack-result' },
body: [response],
}
}
}
}
return { http, db, core }
}