Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1from net.tcp.server import Client 

2from os import tar 

3from os import OsError 

4from os.path import Path 

5from os.subprocess import run 

6from toml import decode as toml_decode 

7from toml import Value as TomlValue 

8from json import decode as json_decode 

9from graphql import parse as graphql_parse 

10from io.buffered_reader import BufferedReader 

11from fiber import Fiber 

12from fiber import Event 

13from http.header_parser import parse_request 

14from http import HttpError 

15from http.header_parser import Request 

16from semver import Version 

17from string import StringBuilder 

18from . import create_token 

19from . import Status 

20from .database import Database 

21from .database import Release 

22from .graphql import GraphQL 

23from .statistics import Statistics 

24from .activities import Activities 

25 

26RE_MYS_VERSION_STANDARD_LIBRARY: regex = ( 

27 re"^/\d+\.\d+\.\d+[\w-]*/standard-library.html") 

28RE_MYS_VERSION_ACTIVITY: regex = re"^/\d+\.\d+\.\d+[\w-]*/activity.html" 

29RE_MYS_VERSION_STATISTICS: regex = re"^/\d+\.\d+\.\d+[\w-]*/statistics.html" 

30RE_MYS_VERSION_WORLD_SVG: regex = re"^/\d+\.\d+\.\d+[\w-]*/_images/world.svg" 

31RE_MYS_VERSION: regex = re"^/\d+\.\d+\.\d+[\w-]*/" 

32RE_PACKAGE_TAR_GZ: regex = re"^/package/([\w-]+)-\d+\.\d+\.\d+[\w-]*.tar.gz$" 

33RE_PACKAGE_LATEST_TAR_GZ: regex = re"^/package/([\w-]+)-latest.tar.gz$" 

34RE_PACKAGE_OPERATIONS: regex = re"^/package/[\w-]+$" 

35RE_PACKAGE_LATEST: regex = re"^/package/[\w-]+/latest/" 

36RE_MYS_TAR_GZ: regex = re"^/mys-\d+\.\d+\.\d+[\w-]*.tar.gz$" 

37RE_PACKAGE_NAME: regex = re"^[\w\-]+$" 

38RE_STANDARD_LIBRARY_BUILD_LOG: regex = re"^/standard-library/([\w-]+)/build-log.html$" 

39RE_STANDARD_LIBRARY_COVERAGE_TAR_GZ: regex = re"^/standard-library/([\w-]+)/coverage.tar.gz$" 

40RE_STANDARD_LIBRARY_COVERAGE: regex = re"^/standard-library/([\w-]+)/coverage/" 

41RE_STANDARD_LIBRARY_BUILD_RESULT: regex = re"^/standard-library/([\w-]+)/build-result.txt$" 

42RE_STANDARD_LIBRARY_DEPENDENTS: regex = re"^/standard-library/([\w-]+)/dependents.txt$" 

43 

44FILE_SUFFIX_TO_CONTENT_TYPE: {string: string} = { 

45 ".html": "text/html", 

46 ".css": "text/css", 

47 ".js": "application/javascript", 

48 ".svg": "image/svg+xml", 

49 ".png": "image/png", 

50 ".woff2": "font/woff2", 

51 ".json": "application/json", 

52} 

53 

54HEADERS_END: bytes = b"\r\n\r\n" 

55 

56STATUS_STRINGS: {i64: string} = { 

57 i64(Status.Continue): "Continue", 

58 i64(Status.Ok): "OK", 

59 i64(Status.Found): "Found", 

60 i64(Status.BadRequest): "Bad Request", 

61 i64(Status.Unauthorized): "Unauthorized", 

62 i64(Status.NotFound): "Not Found", 

63 i64(Status.MethodNotAllowed): "Method Not Allowed" 

64} 

65 

66func builds_to_emoji(result: string) -> string: 

67 match result: 

68 case "yes": 

69 return "✅" 

70 case "no": 

71 return "❌" 

72 case _: 

73 return "🤔" 

74 

75func make_package_link(message: string) -> string: 

76 parts = message.split(" ") 

77 name = parts[1] 

78 parts[1] = f"<a href=\"/package/{name}/latest/index.html\">{name}</a>" 

79 

80 return " ".join(parts) 

81 

82func create_request_table(name: string, requests: {string: i64}) -> string: 

83 row_index = 0 

84 builder = StringBuilder() 

85 builder += ( 

86 "<table class=\"docutils align-default\" " 

87 f"id=\"{name}\">\n" 

88 " <thead>\n" 

89 " <tr class=\"row-odd\">\n" 

90 f" <th class=\"head\" onclick=\"sortTable(0, '{name}')\">" 

91 "Path</th>\n" 

92 f" <th class=\"head\" onclick=\"sortTable(1, '{name}')\">" 

93 "Count</th>\n" 

94 " </tr>\n" 

95 " </thead>\n" 

96 " <tbody>\n" 

97 ) 

98 

99 for path, count in requests: 

100 if (row_index % 2) == 0: 

101 builder += " <tr class=\"row-even\">\n" 

102 else: 

103 builder += " <tr class=\"row-odd\">\n" 

104 

105 builder += f" <td>{path}</td>\n" 

106 builder += f" <td>{count}</td>\n" 

107 builder += " </tr>\n" 

108 row_index += 1 

109 

110 builder += ( 

111 " </tbody>\n" 

112 "</table>\n" 

113 ) 

114 

115 return builder.to_string() 

116 

117SORT_TABLE_JS: string = """\ 

118<script> 

119function sortTable(n, element) { 

120 var table, rows, switching, i, x, y; 

121 var shouldSwitch, dir, switchcount = 0; 

122 table = document.getElementById(element); 

123 switching = true; 

124 dir = \"asc\"; 

125 while (switching) { 

126 switching = false; 

127 rows = table.rows; 

128 for (i = 1; i < (rows.length - 1); i++) { 

129 shouldSwitch = false; 

130 x = rows[i].getElementsByTagName(\"TD\")[n].innerHTML; 

131 y = rows[i + 1].getElementsByTagName(\"TD\")[n].innerHTML; 

132 if (dir == \"asc\") { 

133 if (((n < 1) && (x > y)) 

134 || ((n == 1) && (parseInt(x) > parseInt(y)))) { 

135 shouldSwitch = true; 

136 break; 

137 } 

138 } else if (dir == \"desc\") { 

139 if (((n < 1) && (x < y)) 

140 || ((n == 1) && (parseInt(x) < parseInt(y)))) { 

141 shouldSwitch = true; 

142 break; 

143 } 

144 } 

145 } 

146 if (shouldSwitch) { 

147 rows[i].parentNode.insertBefore(rows[i + 1], rows[i]); 

148 switching = true; 

149 switchcount++; 

150 } else { 

151 if (switchcount == 0 && dir == \"asc\") { 

152 dir = \"desc\"; 

153 switching = true; 

154 } 

155 } 

156 } 

157} 

158</script> 

159""" 

160 

161class ClientHandlerFiber(Fiber): 

162 database: Database 

163 statistics: Statistics 

164 event: Event 

165 client: Client? 

166 idle_client_handlers: [ClientHandlerFiber] 

167 idle_client_handlers_ready: Event 

168 root_directory: Path 

169 response_status: Status 

170 _buffered_reader: BufferedReader? 

171 _graphql: GraphQL 

172 _activities: Activities 

173 

174 func __init__(self, 

175 database: Database, 

176 statistics: Statistics, 

177 graphql: GraphQL, 

178 activities: Activities, 

179 idle_client_handlers: [ClientHandlerFiber], 

180 idle_client_handlers_ready: Event, 

181 index: i64): 

182 self.database = database 

183 self.statistics = statistics 

184 self.idle_client_handlers = idle_client_handlers 

185 self.idle_client_handlers_ready = idle_client_handlers_ready 

186 self.root_directory = Path(f".website/{index}") 

187 self.event = Event() 

188 self.client = None 

189 self.create_root_directory() 

190 self.response_status = Status.Unknown 

191 self._buffered_reader = None 

192 self._graphql = graphql 

193 self._activities = activities 

194 

195 func create_root_directory(self): 

196 self.root_directory.rm(recursive=True, force=True) 

197 self.root_directory.mkdir(exists_ok=True) 

198 

199 func serve_client(self, client: Client): 

200 self.client = client 

201 self._buffered_reader = BufferedReader(client, 1024) 

202 self.event.set() 

203 

204 func run(self): 

205 while True: 

206 self.event.wait() 

207 self.event.clear() 

208 

209 try: 

210 self.serve() 

211 except Error as e: 

212 print(e) 

213 

214 self.client.disconnect() 

215 self.idle_client_handlers.append(self) 

216 self.idle_client_handlers_ready.set() 

217 

218 func make_path(self, path: string) -> Path: 

219 """Prepend the database root directory path to given path. Given path 

220 must not start with a slash. 

221 

222 """ 

223 

224 return self.root_directory.join(path) 

225 

226 func read_header(self) -> bytes?: 

227 return self._buffered_reader.read_until(HEADERS_END) 

228 

229 func handle_request(self, request: Request): 

230 self.response_status = Status.Unknown 

231 

232 if request.path == "/": 

233 request.path = "/index.html" 

234 

235 path = request.path 

236 

237 if path.match(RE_MYS_VERSION_STANDARD_LIBRARY) is not None: 

238 self.handle_mys_version_standard_library(request) 

239 return 

240 

241 if path.match(RE_MYS_VERSION_ACTIVITY) is not None: 

242 self.handle_mys_version_activity(request) 

243 return 

244 

245 if path.match(RE_MYS_VERSION_STATISTICS) is not None: 

246 self.handle_mys_version_statistics(request) 

247 return 

248 

249 if path.match(RE_MYS_VERSION_WORLD_SVG) is not None: 

250 self.handle_mys_version_world_svg(request) 

251 return 

252 

253 if path.match(RE_MYS_VERSION) is not None: 

254 self.handle_mys_version(request) 

255 return 

256 

257 mo = path.match(RE_PACKAGE_TAR_GZ) 

258 

259 if mo is not None: 

260 self.handle_package_tar_gz(request, mo.group(1)) 

261 return 

262 

263 mo = path.match(RE_PACKAGE_LATEST_TAR_GZ) 

264 

265 if mo is not None: 

266 self.handle_package_latest_tar_gz(request, mo.group(1)) 

267 return 

268 

269 if path.match(RE_PACKAGE_OPERATIONS) is not None: 

270 self.handle_package_operations(request) 

271 return 

272 

273 if path.match(RE_PACKAGE_LATEST) is not None: 

274 self.handle_package_latest(request) 

275 return 

276 

277 if path.starts_with("/package/"): 

278 self.handle_package(request) 

279 return 

280 

281 if path.match(RE_MYS_TAR_GZ) is not None: 

282 self.handle_mys_tar_gz(request) 

283 return 

284 

285 if path == "/favicon.ico": 

286 self.handle_static_file(request) 

287 return 

288 

289 if path == "/standard-library.html": 

290 self.handle_mys_standard_library(request) 

291 return 

292 

293 if path == "/activity.html": 

294 self.handle_mys_activity(request) 

295 return 

296 

297 if path == "/statistics.html": 

298 self.handle_mys_statistics(request) 

299 return 

300 

301 if path == "/_images/world.svg": 

302 self.handle_mys_world_svg(request) 

303 return 

304 

305 mo = path.match(RE_STANDARD_LIBRARY_BUILD_RESULT) 

306 

307 if mo is not None: 

308 self.handle_standard_library_build_result(request, mo.group(1)) 

309 return 

310 

311 mo = path.match(RE_STANDARD_LIBRARY_BUILD_LOG) 

312 

313 if mo is not None: 

314 self.handle_standard_library_build_log(request, mo.group(1)) 

315 return 

316 

317 mo = path.match(RE_STANDARD_LIBRARY_COVERAGE_TAR_GZ) 

318 

319 if mo is not None: 

320 self.handle_standard_library_coverage_tar_gz(request, mo.group(1)) 

321 return 

322 

323 if path.match(RE_STANDARD_LIBRARY_COVERAGE) is not None: 

324 self.handle_standard_library_coverage(request) 

325 return 

326 

327 mo = path.match(RE_STANDARD_LIBRARY_DEPENDENTS) 

328 

329 if mo is not None: 

330 self.handle_standard_library_dependents(request, mo.group(1)) 

331 return 

332 

333 if path == "/standard-library/list.txt": 

334 self.handle_standard_library_list(request) 

335 return 

336 

337 if path == "/graphql": 

338 self.handle_graphql(request) 

339 return 

340 

341 self.handle_mys(request) 

342 

343 func serve(self): 

344 header = self.read_header() 

345 

346 if header is None: 

347 return 

348 

349 try: 

350 request = parse_request(header, header.length()) 

351 except HttpError: 

352 self.write_response(Status.BadRequest) 

353 return 

354 

355 # The handler method may change the path, but we want the 

356 # original path in the statistics. 

357 path = request.path 

358 

359 try: 

360 self.handle_request(request) 

361 finally: 

362 request.path = path 

363 self.statistics.handle_request(request, self.response_status) 

364 

365 func handle_mys_version_standard_library(self, request: Request): 

366 match request.method: 

367 case "GET": 

368 path = self.database.make_path(request.path) 

369 

370 if path.exists(): 

371 self.write_static_response_ok(path) 

372 row_index = 0 

373 packages = StringBuilder() 

374 packages += ( 

375 "<table class=\"docutils align-default\">\n" 

376 " <thead>\n" 

377 " <tr class=\"row-odd\">\n" 

378 " <th class=\"head\">Name</th>\n" 

379 " <th class=\"head\">Description</th>\n" 

380 " <th class=\"head\">Version</th>\n" 

381 " <th class=\"head\">Downloads</th>\n" 

382 " <th class=\"head\">Status</th>\n" 

383 " </tr>\n" 

384 " </thead>\n" 

385 " <tbody>\n" 

386 ) 

387 number_of_packages = 0 

388 number_of_downloads = 0 

389 

390 for package_name in self.database.get_packages(): 

391 package = self.database.get_package(package_name) 

392 number_of_packages += 1 

393 number_of_downloads += package.number_of_downloads 

394 builds_emoji = builds_to_emoji(package.builds) 

395 builds_log_path = ( 

396 f"/standard-library/{package_name}/build-log.html") 

397 coverage_path = ( 

398 f"/standard-library/{package_name}/coverage/html/index.html") 

399 database_doc_path = ( 

400 f"/package/{package_name}/latest/index.html") 

401 

402 if (row_index % 2) == 0: 

403 packages += " <tr class=\"row-even\">\n" 

404 else: 

405 packages += " <tr class=\"row-odd\">\n" 

406 

407 packages += ( 

408 f" <td><a href=\"{database_doc_path}\">" 

409 f"{package_name}</a></td>\n") 

410 packages += ( 

411 f" <td>{package.latest_release.description}</td>\n") 

412 packages += f" <td>{package.latest_release.version}</td>\n" 

413 packages += f" <td>{package.number_of_downloads}</td>\n" 

414 packages += " <td>" 

415 packages += ( 

416 f"<a href=\"{builds_log_path}\">{builds_emoji}</a>") 

417 

418 if (package.builds == "yes" 

419 and self.database.make_path(coverage_path).exists()): 

420 packages += ( 

421 f" <a href=\"{coverage_path}\">📄</a>") 

422 

423 packages += "</td>\n" 

424 packages += " </tr>\n" 

425 row_index += 1 

426 

427 packages += (" </tbody>\n" 

428 "</table>\n") 

429 

430 data = path.read_text() 

431 data = data.replace("<p>{website-packages}</p>", 

432 packages.to_string()) 

433 data = data.replace("{website-number-of-packages}", 

434 str(number_of_packages)) 

435 data = data.replace("{website-number-of-downloads}", 

436 str(number_of_downloads)) 

437 self.client.write(data.to_utf8()) 

438 else: 

439 self.write_response(Status.NotFound) 

440 case _: 

441 self.write_response(Status.MethodNotAllowed) 

442 

443 func handle_mys_version_activity(self, request: Request): 

444 match request.method: 

445 case "GET": 

446 path = self.database.make_path(request.path) 

447 

448 if path.exists(): 

449 self.write_static_response_ok(path) 

450 row_index = 0 

451 activities = StringBuilder() 

452 activities += ( 

453 "<table class=\"docutils align-default\">\n" 

454 " <thead>\n" 

455 " <tr class=\"row-odd\">\n" 

456 " <th class=\"head\">Date</th>\n" 

457 " <th class=\"head\">Message</th>\n" 

458 " </tr>\n" 

459 " </thead>\n" 

460 " <tbody>\n") 

461 

462 for activity in self._activities.recent(): 

463 if (row_index % 2) == 0: 

464 activities += " <tr class=\"row-even\">\n" 

465 else: 

466 activities += " <tr class=\"row-odd\">\n" 

467 

468 activities += f" <td>{activity.date}</td>\n" 

469 

470 match activity.kind: 

471 case "📦": 

472 message = make_package_link(activity.message) 

473 case _: 

474 message = activity.message 

475 

476 activities += f" <td>{activity.kind} {message}</td>\n" 

477 activities += " </tr>\n" 

478 row_index += 1 

479 

480 activities += (" </tbody>\n" 

481 "</table>\n") 

482 

483 data = path.read_text() 

484 data = data.replace("<p>{website-activities}</p>", 

485 activities.to_string()) 

486 self.client.write(data.to_utf8()) 

487 else: 

488 self.write_response(Status.NotFound) 

489 case _: 

490 self.write_response(Status.MethodNotAllowed) 

491 

492 func handle_mys_version(self, request: Request): 

493 match request.method: 

494 case "GET": 

495 if ".." in request.path: 

496 self.write_response(Status.BadRequest) 

497 return 

498 

499 path = self.database.make_path(request.path) 

500 

501 if path.exists(): 

502 self.write_static_response_ok(path) 

503 self.client.write(path.read_binary()) 

504 else: 

505 self.write_response(Status.NotFound) 

506 case _: 

507 self.write_response(Status.MethodNotAllowed) 

508 

509 func handle_mys_standard_library(self, request: Request): 

510 mys = self.database.get_mys() 

511 

512 if mys is not None: 

513 request.path = f"/{mys.latest_release.version}{request.path}" 

514 self.handle_mys_version_standard_library(request) 

515 else: 

516 self.write_response(Status.NotFound) 

517 

518 func handle_mys_activity(self, request: Request): 

519 mys = self.database.get_mys() 

520 

521 if mys is not None: 

522 request.path = f"/{mys.latest_release.version}{request.path}" 

523 self.handle_mys_version_activity(request) 

524 else: 

525 self.write_response(Status.NotFound) 

526 

527 func handle_mys_version_statistics(self, request: Request): 

528 match request.method: 

529 case "GET": 

530 path = self.database.make_path(request.path) 

531 

532 if path.exists(): 

533 requests = create_request_table("requestsTable", 

534 self.statistics.requests.count) 

535 row_index = 0 

536 referrers = StringBuilder() 

537 referrers += ( 

538 "<table class=\"docutils align-default\">\n" 

539 " <thead>\n" 

540 " <tr class=\"row-odd\">\n" 

541 " <th class=\"head\">URL</th>\n" 

542 " <th class=\"head\">Count</th>\n" 

543 " </tr>\n" 

544 " </thead>\n" 

545 " <tbody>\n" 

546 ) 

547 

548 for url, count in self.statistics.referrers.count: 

549 if (row_index % 2) == 0: 

550 referrers += " <tr class=\"row-even\">\n" 

551 else: 

552 referrers += " <tr class=\"row-odd\">\n" 

553 

554 referrers += f" <td><a href=\"{url}\">{url}</a></td>\n" 

555 referrers += f" <td>{count}</td>\n" 

556 referrers += " </tr>\n" 

557 row_index += 1 

558 

559 referrers += ( 

560 " </tbody>\n" 

561 "</table>\n" 

562 ) 

563 

564 self.write_response_type(Status.Ok, "text/html") 

565 data = path.read_text() 

566 data = data.replace("{website-start-date-time}", 

567 str(self.statistics.start_date_time)) 

568 data = data.replace("{website-number-of-requests}", 

569 str(self.statistics.number_of_requests)) 

570 data = data.replace("<p>{website-requests}</p>", requests) 

571 data = data.replace( 

572 "{website-number-of-unique-visitors}", 

573 self.statistics.unique_clients()) 

574 data = data.replace("<p>{website-referrers}</p>", 

575 referrers.to_string() + SORT_TABLE_JS) 

576 self.client.write(data.to_utf8()) 

577 else: 

578 self.write_response(Status.NotFound) 

579 case _: 

580 self.write_response(Status.MethodNotAllowed) 

581 

582 func handle_mys_statistics(self, request: Request): 

583 mys = self.database.get_mys() 

584 

585 if mys is not None: 

586 request.path = f"/{mys.latest_release.version}{request.path}" 

587 self.handle_mys_version_statistics(request) 

588 else: 

589 self.write_response(Status.NotFound) 

590 

591 func handle_mys_world_svg(self, request: Request): 

592 mys = self.database.get_mys() 

593 

594 if mys is not None: 

595 request.path = f"/{mys.latest_release.version}{request.path}" 

596 self.handle_mys_version_world_svg(request) 

597 else: 

598 self.write_response(Status.NotFound) 

599 

600 func handle_standard_library_build_result(self, 

601 request: Request, 

602 package_name: string): 

603 match request.method: 

604 case "POST": 

605 content = self.read_post_content(1_000_000, request.headers) 

606 

607 if content is None: 

608 return 

609 

610 builds = string(content) 

611 

612 if builds in ["yes", "no"]: 

613 self.database.set_package_builds(package_name, builds) 

614 self.write_response(Status.Ok) 

615 else: 

616 self.write_response(Status.BadRequest) 

617 case _: 

618 self.write_response(Status.MethodNotAllowed) 

619 

620 func handle_standard_library_build_log(self, 

621 request: Request, 

622 package_name: string): 

623 path = self.database.make_path(request.path) 

624 

625 match request.method: 

626 case "GET": 

627 if path.exists(): 

628 self.write_response(Status.Ok, 

629 headers={"Content-Type": "text/html"}) 

630 self.client.write(path.read_binary()) 

631 else: 

632 self.write_response(Status.NotFound) 

633 case "POST": 

634 package = self.database.get_package(package_name) 

635 

636 if package is None: 

637 self.write_response(Status.NotFound) 

638 return 

639 

640 content = self.read_post_content(5_000_000, request.headers) 

641 

642 if content is None: 

643 return 

644 

645 log_path = self.database.make_path(f"standard-library/{package_name}") 

646 log_path.mkdir(exists_ok=True) 

647 path.write_binary(content) 

648 self.write_response(Status.Ok) 

649 case _: 

650 self.write_response(Status.MethodNotAllowed) 

651 

652 func handle_standard_library_coverage_tar_gz(self, 

653 request: Request, 

654 package_name: string): 

655 match request.method: 

656 case "POST": 

657 package = self.database.get_package(package_name) 

658 

659 if package is None: 

660 self.write_response(Status.NotFound) 

661 return 

662 

663 fiber_path = self.save_post_data_to_file(5_000_000, request.headers) 

664 

665 if fiber_path is None: 

666 return 

667 

668 coverage_path = self.database.make_path( 

669 f"standard-library/{package_name}") 

670 coverage_path.mkdir(exists_ok=True) 

671 tar(fiber_path, 

672 extract=True, 

673 strip_components=2, 

674 output_directory=coverage_path) 

675 self.write_response(Status.Ok) 

676 case _: 

677 self.write_response(Status.MethodNotAllowed) 

678 

679 func handle_standard_library_coverage(self, request: Request): 

680 match request.method: 

681 case "GET": 

682 if ".." in request.path: 

683 self.write_response(Status.BadRequest) 

684 return 

685 

686 path = self.database.make_path(request.path) 

687 

688 if path.exists(): 

689 self.write_static_response_ok(path) 

690 self.client.write(path.read_binary()) 

691 else: 

692 self.write_response(Status.NotFound) 

693 case _: 

694 self.write_response(Status.MethodNotAllowed) 

695 

696 func handle_standard_library_dependents(self, 

697 request: Request, 

698 package_name: string): 

699 match request.method: 

700 case "GET": 

701 package = self.database.get_package(package_name) 

702 

703 if package is None: 

704 self.write_response(Status.NotFound) 

705 else: 

706 dependents = self.database.get_dependents(package_name) 

707 

708 if dependents.length() > 0: 

709 dependents.append("") 

710 

711 self.write_response(Status.Ok, 

712 data="\n".join(dependents).to_utf8()) 

713 case _: 

714 self.write_response(Status.MethodNotAllowed) 

715 

716 func handle_standard_library_list(self, request: Request): 

717 match request.method: 

718 case "GET": 

719 packages = self.database.get_packages() 

720 self.write_response(Status.Ok, data="\n".join(packages).to_utf8()) 

721 case _: 

722 self.write_response(Status.MethodNotAllowed) 

723 

724 func handle_graphql_post(self, request: Request): 

725 content = self.read_post_content(5_000, request.headers) 

726 

727 if content is None: 

728 return 

729 

730 self.statistics.number_of_graphql_requests += 1 

731 

732 try: 

733 if "__schema" in string(content): 

734 path = Path(__assets__).join("schema.json") 

735 self.write_static_response_ok(path) 

736 self.client.write(path.read_binary()) 

737 

738 return 

739 else: 

740 decoded = json_decode(string(content)).get("query").string() 

741 document = graphql_parse(decoded) 

742 response = self._graphql.resolve_query(document) 

743 except Error as error: 

744 message = str(error).replace("\"", "'") 

745 response = f"{{\"errors\":[{{\"message\":\"{message}\"}}]}}" 

746 

747 self.write_response(Status.Ok, 

748 headers={"Content-Type": "application/json"}, 

749 data=response.to_utf8()) 

750 

751 func handle_graphql(self, request: Request): 

752 match request.method: 

753 case "POST": 

754 self.handle_graphql_post(request) 

755 case _: 

756 self.write_response(Status.MethodNotAllowed) 

757 

758 func handle_mys_version_world_svg(self, request: Request): 

759 match request.method: 

760 case "GET": 

761 path = self.database.make_path(request.path) 

762 

763 if path.exists(): 

764 locations = StringBuilder() 

765 

766 for _, location in self.statistics.locations: 

767 # Just approximate x and y. 

768 x = (112.0 / 360.0) * (180.0 + location.longitude) 

769 y = 4.0 + (60.0 / 180.0) * (90.0 - location.latitude) 

770 

771 if location.response_status == Status.Ok: 

772 href = "a" 

773 else: 

774 href = "b" 

775 

776 locations += ( 

777 f""" <use href="#{href}" x="{x}" y="{y}" """ 

778 """style="opacity: 0.6"/>\n""") 

779 

780 self.write_response(Status.Ok, 

781 headers={"Content-Type": "image/svg+xml"}) 

782 world = path.read_text() 

783 world = world.replace(" <!-- {website-world} -->", 

784 locations.to_string()) 

785 self.client.write(world.to_utf8()) 

786 else: 

787 self.write_response(Status.NotFound) 

788 case _: 

789 self.write_response(Status.MethodNotAllowed) 

790 

791 func handle_mys(self, request: Request): 

792 mys = self.database.get_mys() 

793 

794 if mys is not None: 

795 request.path = f"/{mys.latest_release.version}{request.path}" 

796 self.handle_mys_version(request) 

797 else: 

798 self.write_response(Status.NotFound) 

799 

800 func handle_package_operations(self, request: Request): 

801 match request.method: 

802 case "DELETE": 

803 package_name = request.path[9:] 

804 package = self.database.get_package(package_name) 

805 

806 if package is None: 

807 self.write_response(Status.NotFound) 

808 return 

809 

810 if not self.validate_token(request.params, package.token): 

811 return 

812 

813 self.database.delete_package(package) 

814 package_database_path = self.database.make_path( 

815 f"package/{package_name}") 

816 package_database_path.rm(recursive=True, force=True) 

817 

818 for release in package.releases: 

819 release_database_path = self.database.make_path( 

820 f"package/{package.name}-{release.version}.tar.gz") 

821 release_database_path.rm(force=True) 

822 

823 self.write_response(Status.Ok) 

824 self._activities.add("🪦", f"Package {package_name} deleted.") 

825 case _: 

826 self.write_response(Status.MethodNotAllowed) 

827 

828 func handle_package_latest(self, request: Request): 

829 parts = request.path.split('/') 

830 package_name = parts[2] 

831 package = self.database.get_package(package_name) 

832 

833 if package is not None: 

834 request.path = request.path.replace("latest", 

835 package.latest_release.version) 

836 self.handle_mys_version(request) 

837 else: 

838 self.write_response(Status.NotFound) 

839 

840 func handle_package(self, request: Request): 

841 match request.method: 

842 case "GET": 

843 if ".." in request.path: 

844 self.write_response(Status.BadRequest) 

845 return 

846 

847 path = self.database.make_path(request.path) 

848 

849 if path.exists(): 

850 self.write_static_response_ok(path) 

851 self.client.write(path.read_binary()) 

852 else: 

853 self.write_response(Status.NotFound) 

854 case _: 

855 self.write_response(Status.MethodNotAllowed) 

856 

857 func generate_package_documentation(self, 

858 package_name: string, 

859 version: string): 

860 database_doc_path = self.database.make_path( 

861 f"package/{package_name}/{version}") 

862 database_doc_path.rm(recursive=True, force=True) 

863 database_doc_path.mkdir(exists_ok=True) 

864 

865 if self.root_directory.join("doc").exists(): 

866 try: 

867 run(f"mys -C {self.root_directory} doc") 

868 database_doc_path.rm() 

869 self.root_directory.join("build/doc/html").mv(database_doc_path) 

870 except OsError: 

871 data = b"<html>Package documentation build failed!</html>" 

872 database_doc_path.join("index.html").write_binary(data) 

873 else: 

874 data = b"<html>No package documentation found!</html>" 

875 database_doc_path.join("index.html").write_binary(data) 

876 

877 func generate_package_lines_of_code(self, package_name: string): 

878 lines_of_code_path = self.database.make_path( 

879 f"package/{package_name}/lines_of_code.json") 

880 

881 try: 

882 result = run(f"cloc " 

883 f"--read-lang-def={__assets__}/cloc_definitions.txt " 

884 f"--json " 

885 f"{self.root_directory}") 

886 lines_of_code_path.write_binary(result.stdout) 

887 except OsError: 

888 pass 

889 

890 func handle_package_tar_gz(self, request: Request, package_name: string): 

891 match request.method: 

892 case "GET": 

893 self.handle_package_tar_gz_get(package_name, request.path) 

894 case "POST": 

895 self.handle_package_tar_gz_post(request.path, 

896 request.params, 

897 request.headers) 

898 case _: 

899 self.write_response(Status.MethodNotAllowed) 

900 

901 func handle_package_latest_tar_gz(self, 

902 request: Request, 

903 package_name: string): 

904 match request.method: 

905 case "GET": 

906 package = self.database.get_package(package_name) 

907 

908 if package is not None: 

909 version = package.latest_release.version 

910 self.handle_package_tar_gz_get( 

911 package_name, 

912 f"/package/{package_name}-{version}.tar.gz") 

913 else: 

914 self.write_response(Status.NotFound) 

915 case _: 

916 self.write_response(Status.MethodNotAllowed) 

917 

918 func handle_package_tar_gz_get(self, package_name: string, path: string): 

919 database_path = self.database.make_path(path) 

920 

921 if database_path.exists(): 

922 data = database_path.read_binary() 

923 self.write_response(Status.Ok) 

924 self.client.write(data) 

925 self.database.increment_package_download_count(package_name) 

926 else: 

927 self.write_response(Status.NotFound) 

928 

929 func read_post_content(self, 

930 max_size: i64, 

931 headers: {string: string}) -> bytes?: 

932 content_length = i64(headers["content-length"]) 

933 

934 if content_length > max_size: 

935 self.write_response(Status.BadRequest) 

936 

937 return None 

938 

939 expect = headers.get("expect", "") 

940 

941 if expect == "100-continue": 

942 self.write_response(Status.Continue) 

943 

944 if content_length > 0: 

945 data = self._buffered_reader.read(content_length) 

946 

947 if data.length() != content_length: 

948 self.write_response(Status.BadRequest) 

949 

950 return None 

951 else: 

952 data = b"" 

953 

954 return data 

955 

956 func save_post_data_to_file(self, 

957 max_size: i64, 

958 headers: {string: string}) -> Path?: 

959 content = self.read_post_content(max_size, headers) 

960 

961 if content is None: 

962 return None 

963 

964 self.create_root_directory() 

965 fiber_path = self.make_path("archive.tar.gz") 

966 fiber_path.write_binary(content) 

967 

968 return fiber_path 

969 

970 func validate_token(self, 

971 params: {string: string}, 

972 expected_token: string) -> bool: 

973 token = params.get("token", None) 

974 

975 if token is None: 

976 self.write_response(Status.BadRequest) 

977 

978 return False 

979 

980 if token != expected_token: 

981 self.write_response(Status.Unauthorized) 

982 

983 return False 

984 

985 return True 

986 

987 func handle_package_tar_gz_post(self, 

988 path: string, 

989 params: {string: string}, 

990 headers: {string: string}): 

991 fiber_path = self.save_post_data_to_file(50_000_000, headers) 

992 

993 if fiber_path is None: 

994 return 

995 

996 tar(fiber_path, 

997 extract=True, 

998 strip_components=1, 

999 output_directory=self.root_directory) 

1000 package_toml = self.make_path("package.toml").read_text() 

1001 config = toml_decode(package_toml) 

1002 package_name = config.get("package").get("name").string() 

1003 version = config.get("package").get("version").string() 

1004 

1005 try: 

1006 description = config.get("package").get("description").string() 

1007 except KeyError: 

1008 description = "No description found." 

1009 

1010 if package_name.match(RE_PACKAGE_NAME) is None: 

1011 self.write_response(Status.BadRequest) 

1012 return 

1013 

1014 self.database.begin_transaction() 

1015 response_data = "" 

1016 created = False 

1017 

1018 try: 

1019 package = self.database.get_package(package_name) 

1020 

1021 if package is None: 

1022 self.database.create_package(package_name, create_token()) 

1023 package = self.database.get_package(package_name) 

1024 response_data = f"{{\"token\": \"{package.token}\"}}" 

1025 created = True 

1026 elif not self.validate_token(params, package.token): 

1027 self.database.rollback_transaction() 

1028 return 

1029 

1030 self.database.add_package_release(package, version, description) 

1031 release = self.database.get_package_release(package, version) 

1032 

1033 if package.latest_release is None: 

1034 self.database.modify_package(package, release) 

1035 elif Version(version) > Version(package.latest_release.version): 

1036 self.database.modify_package(package, release) 

1037 

1038 self.database.remove_dependents(package_name) 

1039 

1040 try: 

1041 for dependency in config.get("dependencies").table().keys(): 

1042 self.database.add_dependent(dependency, package_name) 

1043 except KeyError: 

1044 pass 

1045 

1046 self.database.commit_transaction() 

1047 except: 

1048 self.database.rollback_transaction() 

1049 raise 

1050 

1051 if created: 

1052 self._activities.add("✨", f"Package {package_name} created.") 

1053 

1054 database_path = self.database.make_path(path) 

1055 database_path.rm(force=True) 

1056 fiber_path.mv(database_path) 

1057 self.generate_package_documentation(package_name, version) 

1058 self.generate_package_lines_of_code(package_name) 

1059 self.write_response(Status.Ok, data=response_data.to_utf8()) 

1060 self._activities.add( 

1061 "📦", 

1062 f"Package {package_name} version {version} released.") 

1063 

1064 func handle_mys_tar_gz(self, request: Request): 

1065 database_path = self.database.make_path(request.path) 

1066 

1067 match request.method: 

1068 case "POST": 

1069 self.handle_mys_tar_gz_post(request, database_path) 

1070 case _: 

1071 self.write_response(Status.MethodNotAllowed) 

1072 

1073 func handle_mys_tar_gz_post(self, request: Request, database_path: Path): 

1074 fiber_path = self.save_post_data_to_file(50_000_000, request.headers) 

1075 

1076 if fiber_path is None: 

1077 return 

1078 

1079 version = request.path[5:-7] 

1080 self.database.make_path(version).rm(recursive=True, force=True) 

1081 tar(fiber_path, 

1082 extract=True, 

1083 output_directory=self.database.root_directory) 

1084 

1085 self.database.begin_transaction() 

1086 response_data = "" 

1087 

1088 try: 

1089 mys = self.database.get_mys() 

1090 

1091 if mys is None: 

1092 self.database.create_mys(create_token()) 

1093 mys = self.database.get_mys() 

1094 response_data = f"{{\"token\": \"{mys.token}\"}}" 

1095 elif not self.validate_token(request.params, mys.token): 

1096 self.database.rollback_transaction() 

1097 return 

1098 

1099 self.database.add_mys_release(version) 

1100 release = self.database.get_mys_release(version) 

1101 

1102 if mys.latest_release is None: 

1103 self.database.modify_mys(release) 

1104 elif Version(version) > Version(mys.latest_release.version): 

1105 self.database.modify_mys(release) 

1106 

1107 self.database.commit_transaction() 

1108 except: 

1109 self.database.rollback_transaction() 

1110 raise 

1111 

1112 self.write_response(Status.Ok, data=response_data.to_utf8()) 

1113 self._activities.add("🐭", f"Mys version {version} released.") 

1114 

1115 func handle_static_file(self, request: Request): 

1116 match request.method: 

1117 case "GET": 

1118 path = Path(__assets__).join(request.path) 

1119 

1120 if path.exists() and ".." not in str(path): 

1121 self.write_static_response_ok(path) 

1122 self.client.write(path.read_binary()) 

1123 else: 

1124 self.write_response(Status.NotFound) 

1125 case _: 

1126 self.write_response(Status.MethodNotAllowed) 

1127 

1128 func write_response(self, 

1129 status: Status, 

1130 headers: {string: string} = {}, 

1131 data: bytes? = None): 

1132 self.response_status = status 

1133 status_string = STATUS_STRINGS[i64(status)] 

1134 self.client.write(f"HTTP/1.1 {status} {status_string}\r\n".to_utf8()) 

1135 

1136 for name, value in headers: 

1137 self.client.write(f"{name}: {value}\r\n".to_utf8()) 

1138 

1139 if data is None: 

1140 self.client.write(b"\r\n") 

1141 else: 

1142 self.client.write(f"Content-Length: {data.length()}\r\n\r\n".to_utf8()) 

1143 self.client.write(data) 

1144 

1145 func write_static_response_ok(self, path: Path): 

1146 content_type = FILE_SUFFIX_TO_CONTENT_TYPE.get(path.extension(), "text/plain") 

1147 

1148 if content_type == "text/html": 

1149 self.write_response_type(Status.Ok, content_type) 

1150 else: 

1151 self.write_response(Status.Ok, 

1152 headers={ 

1153 "Cache-Control": "public, max-age=7200", 

1154 "Content-Type": content_type 

1155 }) 

1156 

1157 func write_response_type(self, status: Status, content_type: string): 

1158 self.write_response(status, 

1159 headers={"Content-Type": content_type})