@joystick.js/db-canary 0.0.0-canary.2209
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/.build/getFilesToBuild.js +26 -0
- package/.build/getPlatformSafeFilePath.js +6 -0
- package/.build/getPlatformSafePath.js +6 -0
- package/.build/index.js +88 -0
- package/.build/isWindows.js +3 -0
- package/API_KEY +1 -0
- package/README.md +1821 -0
- package/data/data.mdb +0 -0
- package/data/lock.mdb +0 -0
- package/dist/client/database.js +1 -0
- package/dist/client/index.js +1 -0
- package/dist/server/cluster/index.js +1 -0
- package/dist/server/cluster/master.js +20 -0
- package/dist/server/cluster/worker.js +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/lib/api_key_manager.js +9 -0
- package/dist/server/lib/auth_manager.js +1 -0
- package/dist/server/lib/auto_index_manager.js +1 -0
- package/dist/server/lib/backup_manager.js +1 -0
- package/dist/server/lib/connection_manager.js +1 -0
- package/dist/server/lib/disk_utils.js +2 -0
- package/dist/server/lib/http_server.js +405 -0
- package/dist/server/lib/index_manager.js +1 -0
- package/dist/server/lib/load_settings.js +1 -0
- package/dist/server/lib/logger.js +1 -0
- package/dist/server/lib/op_types.js +1 -0
- package/dist/server/lib/operation_dispatcher.js +1 -0
- package/dist/server/lib/operations/admin.js +1 -0
- package/dist/server/lib/operations/bulk_write.js +1 -0
- package/dist/server/lib/operations/create_index.js +1 -0
- package/dist/server/lib/operations/delete_one.js +1 -0
- package/dist/server/lib/operations/drop_index.js +1 -0
- package/dist/server/lib/operations/find.js +1 -0
- package/dist/server/lib/operations/find_one.js +1 -0
- package/dist/server/lib/operations/get_indexes.js +1 -0
- package/dist/server/lib/operations/insert_one.js +1 -0
- package/dist/server/lib/operations/update_one.js +1 -0
- package/dist/server/lib/performance_monitor.js +1 -0
- package/dist/server/lib/query_engine.js +1 -0
- package/dist/server/lib/recovery_manager.js +1 -0
- package/dist/server/lib/replication_manager.js +1 -0
- package/dist/server/lib/safe_json_parse.js +1 -0
- package/dist/server/lib/send_response.js +1 -0
- package/dist/server/lib/tcp_protocol.js +1 -0
- package/dist/server/lib/write_forwarder.js +1 -0
- package/dist/server/lib/write_queue.js +1 -0
- package/increment_version.js +3 -0
- package/logs/.013e15b54597d05db4b4b53ecc37b10c92a72927-audit.json +20 -0
- package/logs/.02de550a67ea0f5961faa2dfd458a4d06f59ebd1-audit.json +20 -0
- package/logs/.03494ba24eb3c72214b4068a77d54b8993bee651-audit.json +20 -0
- package/logs/.06309ec60b339be1259a7993dd09c732f8907fbc-audit.json +20 -0
- package/logs/.0663a04dcfa17285661e5e1b8cfa51f41523b210-audit.json +20 -0
- package/logs/.0f06e6c4c9b824622729e13927587479e5060391-audit.json +20 -0
- package/logs/.16ccf58682ecb22b3e3ec63f0da1b7fe9be56528-audit.json +20 -0
- package/logs/.1fa1a5d02f496474b1ab473524c65c984146a9ad-audit.json +20 -0
- package/logs/.2223c0ae3bea6f0d62c62b1d319cc8634856abb7-audit.json +20 -0
- package/logs/.23dc79ffda3e083665e6f5993f59397adcbf4a46-audit.json +20 -0
- package/logs/.28104f49b03906b189eefd1cd462cb46c3c0af22-audit.json +20 -0
- package/logs/.29cdbf13808abe6a0ce70ee2f2efdd680ce3fd8e-audit.json +20 -0
- package/logs/.2a9889afd071f77f41f5170d08703a0afca866b7-audit.json +20 -0
- package/logs/.2acec3d1940a2bbed487528b703ee5948959a599-audit.json +20 -0
- package/logs/.2fb60ff326338c02bfedbcd0e936444e4a216750-audit.json +20 -0
- package/logs/.318fc7a19530d76a345f030f7cad00dda15300e7-audit.json +20 -0
- package/logs/.3cf27043e19085f908cedc7701e6d013463208ee-audit.json +25 -0
- package/logs/.3d90d785415817fc443402843b7c95f8371adc9b-audit.json +20 -0
- package/logs/.4074bca620375f72966fc52dfd439577727671e5-audit.json +20 -0
- package/logs/.40eecf018417ea80a70ea8ec7a3cc9406bc6334b-audit.json +20 -0
- package/logs/.50e974f1ef7c365fca6a1251b2e2c2252914cb5e-audit.json +20 -0
- package/logs/.52cb7d9e4223cf26ba36006ac26b949a97c7923c-audit.json +20 -0
- package/logs/.54befcdb84c15aad980705a31bcc9f555c3577ab-audit.json +20 -0
- package/logs/.57dfb70e22eddb84db2e3c0ceeefac5c0b9baffa-audit.json +20 -0
- package/logs/.5f0b24705a1eaad4eca4968f2d86f91b3f9be683-audit.json +20 -0
- package/logs/.61ba98fdda7db58576b382fee07904e5db1169d6-audit.json +20 -0
- package/logs/.6235017727ef6b199d569a99d6aa8c8e80a1b475-audit.json +20 -0
- package/logs/.63db16193699219489d218a1ddea5dde3750cae4-audit.json +20 -0
- package/logs/.64fb67dfe14149c9eef728d79bf30a54da809c60-audit.json +20 -0
- package/logs/.669137453368987c1f311b5345342527afb54e50-audit.json +20 -0
- package/logs/.7a71f8c89ea28ae266d356aeff6306e876a30fbb-audit.json +20 -0
- package/logs/.7afbaa90fe9dc3a7d682676f9bb79f9a1b1fd9a6-audit.json +20 -0
- package/logs/.7ca29e322cd05327035de850099e7610864f2347-audit.json +20 -0
- package/logs/.83335ab3347e449dae03455a110aaf7f120d4802-audit.json +20 -0
- package/logs/.8c2487b5fd445d2c8e5c483c80b9fa99bbf1ca58-audit.json +20 -0
- package/logs/.8c8b9dc386922c9f3b4c13251af7052aac1d24c0-audit.json +20 -0
- package/logs/.8d6155d94640c4863301ae0fee5e4e7372a21446-audit.json +20 -0
- package/logs/.944a3119a243deea7c8270d5d9e582bb1d0eaa10-audit.json +20 -0
- package/logs/.9816a845c30fb2909f3b26a23eeb3538ebcad5db-audit.json +20 -0
- package/logs/.9dc08784e38b865488177c26d4af5934555e0323-audit.json +20 -0
- package/logs/.9dd27d2e0e454ac0a37600206d1cac5493b0d7ee-audit.json +20 -0
- package/logs/.a3d486feeac7654c59b547de96600e8849a06d4f-audit.json +20 -0
- package/logs/.a5b811f4def22250f86cc18870d7c4573625df22-audit.json +20 -0
- package/logs/.a61648eb5f830e0b6f508ac35e4f8f629d2ad4c7-audit.json +20 -0
- package/logs/.a89016d507045771b4b5a65656944a9c0f1e528b-audit.json +20 -0
- package/logs/.a99bee160a1c590be959af46bacc02724803f691-audit.json +20 -0
- package/logs/.ada7906d6243fd7da802f03d86c4ae5dd9df6236-audit.json +20 -0
- package/logs/.b518339ee942143b6af983af167f5bbb6983b4de-audit.json +20 -0
- package/logs/.b51b124b166d53c9519017856ea610d61d65fabe-audit.json +20 -0
- package/logs/.b7a6aee19f58e55633d5e4a3709041c47dfff975-audit.json +20 -0
- package/logs/.bd7a8a6ba9c55d557a4867ab53f02e3ec2e1553d-audit.json +20 -0
- package/logs/.c1435dafe453b169d6392b25065f3cf4ab6fbb21-audit.json +20 -0
- package/logs/.c17e1ce043109f77dc2f0e2aa290a9d1ed842c03-audit.json +20 -0
- package/logs/.ca62637ce9540e5a38a2fbedb2115febb6ad308a-audit.json +15 -0
- package/logs/.ccee67b9c176967f8977071409a41f5cb5cd6ad4-audit.json +20 -0
- package/logs/.db24043417ea79a6f14cd947476399e53930b48d-audit.json +20 -0
- package/logs/.e0f12acccb57829f5f33712bb2e2607ecd808147-audit.json +20 -0
- package/logs/.e9b6cc33d0bbd2e644c4e2bf44d177f850016557-audit.json +20 -0
- package/logs/.f15291d434808e3bdca7963ccd2e73893be027e6-audit.json +20 -0
- package/logs/.f4bdf9e21ef84f8a3fae3ffb32bbc39275991351-audit.json +15 -0
- package/logs/.fbac3aefac1e81b4230df5aa50667cb90d51024f-audit.json +20 -0
- package/logs/.fcfd495c0a9169db243f4a4f21878ee02b76413c-audit.json +20 -0
- package/logs/admin-2025-09-12.log +580 -0
- package/logs/admin-2025-09-15.log +283 -0
- package/logs/admin-error-2025-09-12.log +22 -0
- package/logs/admin-error-2025-09-15.log +10 -0
- package/logs/api_key_manager-2025-09-12.log +658 -0
- package/logs/api_key_manager-2025-09-15.log +295 -0
- package/logs/api_key_manager-error-2025-09-12.log +0 -0
- package/logs/api_key_manager-error-2025-09-15.log +0 -0
- package/logs/auth_manager-2025-09-12.log +4432 -0
- package/logs/auth_manager-2025-09-15.log +2000 -0
- package/logs/auth_manager-error-2025-09-12.log +11 -0
- package/logs/auth_manager-error-2025-09-15.log +5 -0
- package/logs/auto_index_manager-2025-09-12.log +84 -0
- package/logs/auto_index_manager-2025-09-15.log +45 -0
- package/logs/auto_index_manager-error-2025-09-12.log +6 -0
- package/logs/auto_index_manager-error-2025-09-15.log +0 -0
- package/logs/backup_manager-2025-09-12.log +198 -0
- package/logs/backup_manager-2025-09-15.log +90 -0
- package/logs/backup_manager-error-2025-09-12.log +198 -0
- package/logs/backup_manager-error-2025-09-15.log +90 -0
- package/logs/bulk_write-2025-09-12.log +66 -0
- package/logs/bulk_write-2025-09-15.log +38 -0
- package/logs/bulk_write-error-2025-09-12.log +0 -0
- package/logs/bulk_write-error-2025-09-15.log +0 -0
- package/logs/connection_manager-2025-09-12.log +2412 -0
- package/logs/connection_manager-2025-09-15.log +1132 -0
- package/logs/connection_manager-error-2025-09-12.log +0 -0
- package/logs/connection_manager-error-2025-09-15.log +0 -0
- package/logs/create_index-2025-09-12.log +302 -0
- package/logs/create_index-2025-09-15.log +158 -0
- package/logs/create_index-error-2025-09-12.log +30 -0
- package/logs/create_index-error-2025-09-15.log +13 -0
- package/logs/delete_one-2025-09-12.log +73 -0
- package/logs/delete_one-2025-09-15.log +43 -0
- package/logs/delete_one-error-2025-09-12.log +0 -0
- package/logs/delete_one-error-2025-09-15.log +0 -0
- package/logs/disk_utils-2025-09-12.log +4954 -0
- package/logs/disk_utils-2025-09-15.log +2446 -0
- package/logs/disk_utils-error-2025-09-12.log +0 -0
- package/logs/disk_utils-error-2025-09-15.log +0 -0
- package/logs/drop_index-2025-09-12.log +41 -0
- package/logs/drop_index-2025-09-15.log +23 -0
- package/logs/drop_index-error-2025-09-12.log +11 -0
- package/logs/drop_index-error-2025-09-15.log +5 -0
- package/logs/find-2025-09-12.log +1050 -0
- package/logs/find-2025-09-15.log +592 -0
- package/logs/find-error-2025-09-12.log +1 -0
- package/logs/find-error-2025-09-15.log +0 -0
- package/logs/find_one-2025-09-12.log +425 -0
- package/logs/find_one-2025-09-15.log +264 -0
- package/logs/find_one-error-2025-09-12.log +5 -0
- package/logs/find_one-error-2025-09-15.log +0 -0
- package/logs/get_indexes-2025-09-12.log +84 -0
- package/logs/get_indexes-2025-09-15.log +56 -0
- package/logs/get_indexes-error-2025-09-12.log +6 -0
- package/logs/get_indexes-error-2025-09-15.log +0 -0
- package/logs/http_server-2025-09-12.log +2772 -0
- package/logs/http_server-2025-09-15.log +1276 -0
- package/logs/http_server-error-2025-09-12.log +212 -0
- package/logs/http_server-error-2025-09-15.log +44 -0
- package/logs/index_manager-2025-09-12.log +5031 -0
- package/logs/index_manager-2025-09-15.log +2909 -0
- package/logs/index_manager-error-2025-09-12.log +80 -0
- package/logs/index_manager-error-2025-09-15.log +38 -0
- package/logs/insert_one-2025-09-12.log +2181 -0
- package/logs/insert_one-2025-09-15.log +1293 -0
- package/logs/insert_one-error-2025-09-12.log +0 -0
- package/logs/insert_one-error-2025-09-15.log +0 -0
- package/logs/master-2025-09-12.log +1882 -0
- package/logs/master-2025-09-15.log +910 -0
- package/logs/master-error-2025-09-12.log +80 -0
- package/logs/master-error-2025-09-15.log +0 -0
- package/logs/operation_dispatcher-2025-09-12.log +751 -0
- package/logs/operation_dispatcher-2025-09-15.log +359 -0
- package/logs/operation_dispatcher-error-2025-09-12.log +33 -0
- package/logs/operation_dispatcher-error-2025-09-15.log +11 -0
- package/logs/performance_monitor-2025-09-12.log +14889 -0
- package/logs/performance_monitor-2025-09-15.log +6803 -0
- package/logs/performance_monitor-error-2025-09-12.log +0 -0
- package/logs/performance_monitor-error-2025-09-15.log +0 -0
- package/logs/query_engine-2025-09-12.log +5310 -0
- package/logs/query_engine-2025-09-15.log +2639 -0
- package/logs/query_engine-error-2025-09-12.log +0 -0
- package/logs/query_engine-error-2025-09-15.log +0 -0
- package/logs/recovery_manager-2025-09-12.log +462 -0
- package/logs/recovery_manager-2025-09-15.log +210 -0
- package/logs/recovery_manager-error-2025-09-12.log +22 -0
- package/logs/recovery_manager-error-2025-09-15.log +10 -0
- package/logs/replication-2025-09-12.log +1923 -0
- package/logs/replication-2025-09-15.log +917 -0
- package/logs/replication-error-2025-09-12.log +33 -0
- package/logs/replication-error-2025-09-15.log +15 -0
- package/logs/server-2025-09-12.log +2601 -0
- package/logs/server-2025-09-15.log +1191 -0
- package/logs/server-error-2025-09-12.log +0 -0
- package/logs/server-error-2025-09-15.log +0 -0
- package/logs/tcp_protocol-2025-09-12.log +22 -0
- package/logs/tcp_protocol-2025-09-15.log +10 -0
- package/logs/tcp_protocol-error-2025-09-12.log +22 -0
- package/logs/tcp_protocol-error-2025-09-15.log +10 -0
- package/logs/test-2025-09-12.log +0 -0
- package/logs/test-2025-09-15.log +0 -0
- package/logs/test-error-2025-09-12.log +0 -0
- package/logs/test-error-2025-09-15.log +0 -0
- package/logs/update_one-2025-09-12.log +173 -0
- package/logs/update_one-2025-09-15.log +118 -0
- package/logs/update_one-error-2025-09-12.log +0 -0
- package/logs/update_one-error-2025-09-15.log +0 -0
- package/logs/worker-2025-09-12.log +1457 -0
- package/logs/worker-2025-09-15.log +695 -0
- package/logs/worker-error-2025-09-12.log +0 -0
- package/logs/worker-error-2025-09-15.log +0 -0
- package/logs/write_forwarder-2025-09-12.log +1956 -0
- package/logs/write_forwarder-2025-09-15.log +932 -0
- package/logs/write_forwarder-error-2025-09-12.log +66 -0
- package/logs/write_forwarder-error-2025-09-15.log +30 -0
- package/logs/write_queue-2025-09-12.log +612 -0
- package/logs/write_queue-2025-09-15.log +301 -0
- package/logs/write_queue-error-2025-09-12.log +184 -0
- package/logs/write_queue-error-2025-09-15.log +83 -0
- package/package.json +48 -0
- package/prompts/01-core-infrastructure.md +56 -0
- package/prompts/02-secondary-indexing.md +65 -0
- package/prompts/03-write-serialization.md +63 -0
- package/prompts/04-enhanced-authentication.md +75 -0
- package/prompts/05-comprehensive-admin-operations.md +75 -0
- package/prompts/06-backup-and-restore-system.md +106 -0
- package/prompts/07-production-safety-features.md +107 -0
- package/prompts/08-tcp-client-library.md +121 -0
- package/prompts/09-api-method-chaining.md +134 -0
- package/prompts/10-automatic-index-creation.md +223 -0
- package/prompts/11-operation-naming-consistency.md +268 -0
- package/prompts/12-tcp-replication-system.md +333 -0
- package/prompts/13-master-read-write-operations.md +57 -0
- package/prompts/14-index-upsert-operations.md +68 -0
- package/prompts/15-client-api-return-types.md +81 -0
- package/prompts/16-server-setup-ui.md +97 -0
- package/prompts/17-emergency-password-change.md +108 -0
- package/prompts/18-joystick-framework-integration.md +116 -0
- package/prompts/19-api-key-authentication-system.md +137 -0
- package/prompts/20-configurable-server-port.md +105 -0
- package/prompts/21-multi-database-support.md +161 -0
- package/prompts/FULL_TEXT_SEARCH.md +293 -0
- package/prompts/PROMPTS.md +158 -0
- package/prompts/README.md +221 -0
- package/prompts/TYPESCRIPT_GENERATION.md +179 -0
- package/src/client/database.js +166 -0
- package/src/client/index.js +752 -0
- package/src/server/cluster/index.js +53 -0
- package/src/server/cluster/master.js +774 -0
- package/src/server/cluster/worker.js +537 -0
- package/src/server/index.js +540 -0
- package/src/server/lib/api_key_manager.js +473 -0
- package/src/server/lib/auth_manager.js +375 -0
- package/src/server/lib/auto_index_manager.js +681 -0
- package/src/server/lib/backup_manager.js +650 -0
- package/src/server/lib/connection_manager.js +218 -0
- package/src/server/lib/disk_utils.js +118 -0
- package/src/server/lib/http_server.js +1165 -0
- package/src/server/lib/index_manager.js +756 -0
- package/src/server/lib/load_settings.js +143 -0
- package/src/server/lib/logger.js +135 -0
- package/src/server/lib/op_types.js +29 -0
- package/src/server/lib/operation_dispatcher.js +268 -0
- package/src/server/lib/operations/admin.js +808 -0
- package/src/server/lib/operations/bulk_write.js +367 -0
- package/src/server/lib/operations/create_index.js +68 -0
- package/src/server/lib/operations/delete_one.js +114 -0
- package/src/server/lib/operations/drop_index.js +58 -0
- package/src/server/lib/operations/find.js +340 -0
- package/src/server/lib/operations/find_one.js +319 -0
- package/src/server/lib/operations/get_indexes.js +52 -0
- package/src/server/lib/operations/insert_one.js +113 -0
- package/src/server/lib/operations/update_one.js +225 -0
- package/src/server/lib/performance_monitor.js +313 -0
- package/src/server/lib/query_engine.js +243 -0
- package/src/server/lib/recovery_manager.js +388 -0
- package/src/server/lib/replication_manager.js +727 -0
- package/src/server/lib/safe_json_parse.js +21 -0
- package/src/server/lib/send_response.js +47 -0
- package/src/server/lib/tcp_protocol.js +130 -0
- package/src/server/lib/write_forwarder.js +636 -0
- package/src/server/lib/write_queue.js +335 -0
- package/test_data/data.mdb +0 -0
- package/test_data/lock.mdb +0 -0
- package/tests/client/index.test.js +1232 -0
- package/tests/server/cluster/cluster.test.js +248 -0
- package/tests/server/cluster/master_read_write_operations.test.js +577 -0
- package/tests/server/index.test.js +651 -0
- package/tests/server/integration/authentication_integration.test.js +294 -0
- package/tests/server/integration/auto_indexing_integration.test.js +268 -0
- package/tests/server/integration/backup_integration.test.js +513 -0
- package/tests/server/integration/indexing_integration.test.js +126 -0
- package/tests/server/integration/production_safety_integration.test.js +358 -0
- package/tests/server/integration/replication_integration.test.js +227 -0
- package/tests/server/integration/write_serialization_integration.test.js +246 -0
- package/tests/server/lib/api_key_manager.test.js +516 -0
- package/tests/server/lib/auth_manager.test.js +317 -0
- package/tests/server/lib/auto_index_manager.test.js +275 -0
- package/tests/server/lib/backup_manager.test.js +238 -0
- package/tests/server/lib/connection_manager.test.js +221 -0
- package/tests/server/lib/disk_utils.test.js +63 -0
- package/tests/server/lib/http_server.test.js +389 -0
- package/tests/server/lib/index_manager.test.js +301 -0
- package/tests/server/lib/load_settings.test.js +107 -0
- package/tests/server/lib/load_settings_port_config.test.js +243 -0
- package/tests/server/lib/logger.test.js +282 -0
- package/tests/server/lib/operations/admin.test.js +638 -0
- package/tests/server/lib/operations/bulk_write.test.js +128 -0
- package/tests/server/lib/operations/create_index.test.js +138 -0
- package/tests/server/lib/operations/delete_one.test.js +52 -0
- package/tests/server/lib/operations/drop_index.test.js +72 -0
- package/tests/server/lib/operations/find.test.js +93 -0
- package/tests/server/lib/operations/find_one.test.js +91 -0
- package/tests/server/lib/operations/get_indexes.test.js +87 -0
- package/tests/server/lib/operations/insert_one.test.js +42 -0
- package/tests/server/lib/operations/update_one.test.js +89 -0
- package/tests/server/lib/performance_monitor.test.js +185 -0
- package/tests/server/lib/query_engine.test.js +46 -0
- package/tests/server/lib/recovery_manager.test.js +414 -0
- package/tests/server/lib/replication_manager.test.js +202 -0
- package/tests/server/lib/safe_json_parse.test.js +45 -0
- package/tests/server/lib/send_response.test.js +155 -0
- package/tests/server/lib/tcp_protocol.test.js +169 -0
- package/tests/server/lib/write_forwarder.test.js +258 -0
- package/tests/server/lib/write_queue.test.js +255 -0
- package/tsconfig.json +30 -0
- package/types/client/index.d.ts +447 -0
- package/types/server/cluster/index.d.ts +28 -0
- package/types/server/cluster/master.d.ts +115 -0
- package/types/server/cluster/worker.d.ts +1 -0
- package/types/server/lib/auth_manager.d.ts +13 -0
- package/types/server/lib/backup_manager.d.ts +43 -0
- package/types/server/lib/connection_manager.d.ts +15 -0
- package/types/server/lib/disk_utils.d.ts +3 -0
- package/types/server/lib/index_manager.d.ts +24 -0
- package/types/server/lib/load_settings.d.ts +4 -0
- package/types/server/lib/logger.d.ts +44 -0
- package/types/server/lib/op_types.d.ts +6 -0
- package/types/server/lib/performance_monitor.d.ts +68 -0
- package/types/server/lib/query_engine.d.ts +10 -0
- package/types/server/lib/safe_json_parse.d.ts +7 -0
- package/types/server/lib/send_response.d.ts +3 -0
- package/types/server/lib/tcp_protocol.d.ts +12 -0
- package/types/server/lib/write_queue.d.ts +2 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Backup and restore management system for JoystickDB with S3 integration.
|
|
3
|
+
* Provides comprehensive backup creation, restoration, scheduling, and cleanup functionality
|
|
4
|
+
* with support for AWS S3 storage, checksum verification, and automated retention policies.
|
|
5
|
+
* Includes sparse backup compression, integrity verification, and graceful error handling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { S3Client, PutObjectCommand, GetObjectCommand, ListObjectsV2Command, DeleteObjectCommand } from '@aws-sdk/client-s3';
|
|
9
|
+
import { createReadStream, createWriteStream, existsSync, unlinkSync, statSync } from 'fs';
|
|
10
|
+
import { mkdir, rm } from 'fs/promises';
|
|
11
|
+
import { resolve, join } from 'path';
|
|
12
|
+
import { spawn } from 'child_process';
|
|
13
|
+
import { createHash } from 'crypto';
|
|
14
|
+
import { get_settings } from './load_settings.js';
|
|
15
|
+
import { get_database } from './query_engine.js';
|
|
16
|
+
import create_logger from './logger.js';
|
|
17
|
+
|
|
18
|
+
const { create_context_logger } = create_logger('backup_manager');
|
|
19
|
+
|
|
20
|
+
/** @type {S3Client|null} AWS S3 client instance for backup operations */
|
|
21
|
+
let s3_client = null;
|
|
22
|
+
|
|
23
|
+
/** @type {NodeJS.Timeout|null} Timer for scheduled backup operations */
|
|
24
|
+
let backup_schedule_timer = null;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initializes and returns the S3 client with bucket configuration.
|
|
28
|
+
* Creates a new S3 client instance if one doesn't exist, using settings from configuration.
|
|
29
|
+
* Supports custom endpoints for S3-compatible services.
|
|
30
|
+
* @returns {Object} Object containing S3 client and bucket name
|
|
31
|
+
* @returns {S3Client} returns.client - Configured S3 client instance
|
|
32
|
+
* @returns {string} returns.bucket - S3 bucket name for backups
|
|
33
|
+
* @throws {Error} When S3 configuration is missing or invalid
|
|
34
|
+
*/
|
|
35
|
+
const get_s3_client = () => {
|
|
36
|
+
const log = create_context_logger();
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const settings = get_settings();
|
|
40
|
+
|
|
41
|
+
if (!settings.s3) {
|
|
42
|
+
throw new Error('S3 configuration not found in settings');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { bucket, region, access_key, secret_key, endpoint } = settings.s3;
|
|
46
|
+
|
|
47
|
+
if (!bucket || !region || !access_key || !secret_key) {
|
|
48
|
+
throw new Error('Missing required S3 configuration: bucket, region, access_key, secret_key');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!s3_client) {
|
|
52
|
+
const client_config = {
|
|
53
|
+
region,
|
|
54
|
+
credentials: {
|
|
55
|
+
accessKeyId: access_key,
|
|
56
|
+
secretAccessKey: secret_key
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (endpoint && endpoint !== `https://s3.${region}.amazonaws.com`) {
|
|
61
|
+
client_config.endpoint = endpoint;
|
|
62
|
+
client_config.forcePathStyle = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
s3_client = new S3Client(client_config);
|
|
66
|
+
log.info('S3 client initialized', { bucket, region, endpoint: endpoint || 'default' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { client: s3_client, bucket };
|
|
70
|
+
} catch (error) {
|
|
71
|
+
log.error('Failed to initialize S3 client', { error: error.message });
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Tests the S3 connection by attempting to list objects in the configured bucket.
|
|
78
|
+
* Validates that the S3 client can successfully communicate with the storage service.
|
|
79
|
+
* @returns {Promise<boolean>} True if connection test succeeds
|
|
80
|
+
* @throws {Error} When S3 connection fails or configuration is invalid
|
|
81
|
+
*/
|
|
82
|
+
const test_s3_connection = async () => {
|
|
83
|
+
const log = create_context_logger();
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const { client, bucket } = get_s3_client();
|
|
87
|
+
|
|
88
|
+
const command = new ListObjectsV2Command({
|
|
89
|
+
Bucket: bucket,
|
|
90
|
+
MaxKeys: 1
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await client.send(command);
|
|
94
|
+
log.info('S3 connection test successful', { bucket });
|
|
95
|
+
return true;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
log.error('S3 connection test failed', { error: error.message });
|
|
98
|
+
throw new Error(`S3 connection failed: ${error.message}`);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Calculates SHA256 checksum for a file to verify integrity.
|
|
104
|
+
* Streams the file content to avoid loading large files into memory.
|
|
105
|
+
* @param {string} file_path - Path to the file to calculate checksum for
|
|
106
|
+
* @returns {Promise<string>} SHA256 checksum as hexadecimal string
|
|
107
|
+
* @throws {Error} When file cannot be read or checksum calculation fails
|
|
108
|
+
*/
|
|
109
|
+
const calculate_sha256 = (file_path) => {
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
const hash = createHash('sha256');
|
|
112
|
+
const stream = createReadStream(file_path);
|
|
113
|
+
|
|
114
|
+
stream.on('data', (data) => hash.update(data));
|
|
115
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
116
|
+
stream.on('error', reject);
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Creates a compressed backup of the database and uploads it to S3.
|
|
122
|
+
* Generates a timestamped tar.gz archive with sparse file support, calculates checksums,
|
|
123
|
+
* uploads to S3 with metadata, and cleans up temporary files.
|
|
124
|
+
* @returns {Promise<Object>} Backup creation result
|
|
125
|
+
* @returns {string} returns.filename - Generated backup filename
|
|
126
|
+
* @returns {number} returns.size_bytes - Backup file size in bytes
|
|
127
|
+
* @returns {string} returns.checksum - SHA256 checksum of backup file
|
|
128
|
+
* @returns {string} returns.created_at - ISO timestamp of backup creation
|
|
129
|
+
* @returns {number} returns.duration_ms - Time taken to create backup in milliseconds
|
|
130
|
+
* @returns {string} returns.s3_location - S3 URI of uploaded backup
|
|
131
|
+
* @throws {Error} When backup creation, compression, or upload fails
|
|
132
|
+
*/
|
|
133
|
+
const create_backup = async () => {
|
|
134
|
+
const log = create_context_logger();
|
|
135
|
+
const backup_start = Date.now();
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
// NOTE: Ensure S3 is configured.
|
|
139
|
+
const { client, bucket } = get_s3_client();
|
|
140
|
+
|
|
141
|
+
// NOTE: Create temporary directory.
|
|
142
|
+
const temp_dir = resolve('./temp_backup');
|
|
143
|
+
await mkdir(temp_dir, { recursive: true });
|
|
144
|
+
|
|
145
|
+
// NOTE: Generate backup filename.
|
|
146
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
147
|
+
const backup_filename = `joystickdb-backup-${timestamp}.tar.gz`;
|
|
148
|
+
const temp_backup_path = join(temp_dir, backup_filename);
|
|
149
|
+
|
|
150
|
+
log.info('Starting backup creation', { backup_filename, temp_backup_path });
|
|
151
|
+
|
|
152
|
+
// NOTE: Create compressed sparse backup.
|
|
153
|
+
const tar_process = spawn('tar', [
|
|
154
|
+
'--sparse',
|
|
155
|
+
'-czf',
|
|
156
|
+
temp_backup_path,
|
|
157
|
+
'-C',
|
|
158
|
+
'./data',
|
|
159
|
+
'.'
|
|
160
|
+
], {
|
|
161
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
let tar_output = '';
|
|
165
|
+
let tar_error = '';
|
|
166
|
+
|
|
167
|
+
tar_process.stdout.on('data', (data) => {
|
|
168
|
+
tar_output += data.toString();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
tar_process.stderr.on('data', (data) => {
|
|
172
|
+
tar_error += data.toString();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const tar_exit_code = await new Promise((resolve) => {
|
|
176
|
+
tar_process.on('close', resolve);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (tar_exit_code !== 0) {
|
|
180
|
+
throw new Error(`Tar process failed with exit code ${tar_exit_code}: ${tar_error}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// NOTE: Calculate checksum.
|
|
184
|
+
const checksum = await calculate_sha256(temp_backup_path);
|
|
185
|
+
const backup_stats = statSync(temp_backup_path);
|
|
186
|
+
|
|
187
|
+
log.info('Backup file created', {
|
|
188
|
+
backup_filename,
|
|
189
|
+
size_bytes: backup_stats.size,
|
|
190
|
+
size_mb: Math.round(backup_stats.size / 1024 / 1024),
|
|
191
|
+
checksum
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// NOTE: Upload to S3.
|
|
195
|
+
const upload_stream = createReadStream(temp_backup_path);
|
|
196
|
+
|
|
197
|
+
const upload_command = new PutObjectCommand({
|
|
198
|
+
Bucket: bucket,
|
|
199
|
+
Key: backup_filename,
|
|
200
|
+
Body: upload_stream,
|
|
201
|
+
Metadata: {
|
|
202
|
+
checksum,
|
|
203
|
+
created_at: new Date().toISOString(),
|
|
204
|
+
size_bytes: backup_stats.size.toString()
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
await client.send(upload_command);
|
|
209
|
+
|
|
210
|
+
// NOTE: Clean up temporary file.
|
|
211
|
+
unlinkSync(temp_backup_path);
|
|
212
|
+
await rm(temp_dir, { recursive: true, force: true });
|
|
213
|
+
|
|
214
|
+
const backup_duration = Date.now() - backup_start;
|
|
215
|
+
|
|
216
|
+
log.info('Backup completed successfully', {
|
|
217
|
+
backup_filename,
|
|
218
|
+
duration_ms: backup_duration,
|
|
219
|
+
size_mb: Math.round(backup_stats.size / 1024 / 1024),
|
|
220
|
+
checksum,
|
|
221
|
+
s3_bucket: bucket
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
filename: backup_filename,
|
|
226
|
+
size_bytes: backup_stats.size,
|
|
227
|
+
checksum,
|
|
228
|
+
created_at: new Date().toISOString(),
|
|
229
|
+
duration_ms: backup_duration,
|
|
230
|
+
s3_location: `s3://${bucket}/${backup_filename}`
|
|
231
|
+
};
|
|
232
|
+
} catch (error) {
|
|
233
|
+
log.error('Backup creation failed', { error: error.message });
|
|
234
|
+
|
|
235
|
+
// NOTE: Clean up on failure.
|
|
236
|
+
try {
|
|
237
|
+
const temp_dir = resolve('./temp_backup');
|
|
238
|
+
if (existsSync(temp_dir)) {
|
|
239
|
+
await rm(temp_dir, { recursive: true, force: true });
|
|
240
|
+
}
|
|
241
|
+
} catch (cleanup_error) {
|
|
242
|
+
log.warn('Failed to clean up temporary backup files', { error: cleanup_error.message });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Lists all available backups from S3 storage with metadata.
|
|
251
|
+
* Retrieves backup files, sorts by modification date, and calculates total storage usage.
|
|
252
|
+
* @returns {Promise<Object>} Backup listing result
|
|
253
|
+
* @returns {Array<Object>} returns.backups - Array of backup objects
|
|
254
|
+
* @returns {string} returns.backups[].filename - Backup filename
|
|
255
|
+
* @returns {number} returns.backups[].size_bytes - File size in bytes
|
|
256
|
+
* @returns {number} returns.backups[].size_mb - File size in megabytes (rounded)
|
|
257
|
+
* @returns {string} returns.backups[].last_modified - ISO timestamp of last modification
|
|
258
|
+
* @returns {string} returns.backups[].s3_location - S3 URI of backup file
|
|
259
|
+
* @returns {number} returns.total_count - Total number of backups
|
|
260
|
+
* @returns {number} returns.total_size_bytes - Total storage used in bytes
|
|
261
|
+
* @returns {number} returns.total_size_mb - Total storage used in megabytes (rounded)
|
|
262
|
+
* @throws {Error} When S3 listing operation fails
|
|
263
|
+
*/
|
|
264
|
+
const list_backups = async () => {
|
|
265
|
+
const log = create_context_logger();
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const { client, bucket } = get_s3_client();
|
|
269
|
+
|
|
270
|
+
const command = new ListObjectsV2Command({
|
|
271
|
+
Bucket: bucket,
|
|
272
|
+
Prefix: 'joystickdb-backup-'
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const response = await client.send(command);
|
|
276
|
+
|
|
277
|
+
if (!response.Contents || response.Contents.length === 0) {
|
|
278
|
+
return {
|
|
279
|
+
backups: [],
|
|
280
|
+
total_count: 0,
|
|
281
|
+
total_size_bytes: 0
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const backups = response.Contents
|
|
286
|
+
.filter(obj => obj.Key.startsWith('joystickdb-backup-') && obj.Key.endsWith('.tar.gz'))
|
|
287
|
+
.map(obj => ({
|
|
288
|
+
filename: obj.Key,
|
|
289
|
+
size_bytes: obj.Size,
|
|
290
|
+
size_mb: Math.round(obj.Size / 1024 / 1024),
|
|
291
|
+
last_modified: obj.LastModified.toISOString(),
|
|
292
|
+
s3_location: `s3://${bucket}/${obj.Key}`
|
|
293
|
+
}))
|
|
294
|
+
.sort((a, b) => new Date(b.last_modified) - new Date(a.last_modified));
|
|
295
|
+
|
|
296
|
+
const total_size_bytes = backups.reduce((sum, backup) => sum + backup.size_bytes, 0);
|
|
297
|
+
|
|
298
|
+
log.info('Listed backups', {
|
|
299
|
+
total_count: backups.length,
|
|
300
|
+
total_size_mb: Math.round(total_size_bytes / 1024 / 1024)
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
backups,
|
|
305
|
+
total_count: backups.length,
|
|
306
|
+
total_size_bytes,
|
|
307
|
+
total_size_mb: Math.round(total_size_bytes / 1024 / 1024)
|
|
308
|
+
};
|
|
309
|
+
} catch (error) {
|
|
310
|
+
log.error('Failed to list backups', { error: error.message });
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Restores database from a specified backup file stored in S3.
|
|
317
|
+
* Downloads backup, verifies checksum, creates safety backup of current data,
|
|
318
|
+
* extracts backup to data directory, and handles rollback on failure.
|
|
319
|
+
* @param {string} backup_filename - Name of the backup file to restore
|
|
320
|
+
* @returns {Promise<Object>} Restore operation result
|
|
321
|
+
* @returns {string} returns.backup_filename - Name of restored backup file
|
|
322
|
+
* @returns {string} returns.restored_at - ISO timestamp of restore completion
|
|
323
|
+
* @returns {number} returns.duration_ms - Time taken to restore in milliseconds
|
|
324
|
+
* @returns {string} returns.status - Restore status ('success')
|
|
325
|
+
* @throws {Error} When backup download, verification, or extraction fails
|
|
326
|
+
*/
|
|
327
|
+
const restore_backup = async (backup_filename) => {
|
|
328
|
+
const log = create_context_logger();
|
|
329
|
+
const restore_start = Date.now();
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
const { client, bucket } = get_s3_client();
|
|
333
|
+
|
|
334
|
+
// NOTE: Create temporary directory.
|
|
335
|
+
const temp_dir = resolve('./temp_restore');
|
|
336
|
+
await mkdir(temp_dir, { recursive: true });
|
|
337
|
+
|
|
338
|
+
const temp_backup_path = join(temp_dir, backup_filename);
|
|
339
|
+
|
|
340
|
+
log.info('Starting backup restore', { backup_filename, temp_backup_path });
|
|
341
|
+
|
|
342
|
+
// NOTE: Download backup from S3.
|
|
343
|
+
const download_command = new GetObjectCommand({
|
|
344
|
+
Bucket: bucket,
|
|
345
|
+
Key: backup_filename
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const response = await client.send(download_command);
|
|
349
|
+
|
|
350
|
+
if (!response.Body) {
|
|
351
|
+
throw new Error('Empty backup file received from S3');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// NOTE: Stream backup to temporary file.
|
|
355
|
+
const write_stream = createWriteStream(temp_backup_path);
|
|
356
|
+
|
|
357
|
+
await new Promise((resolve, reject) => {
|
|
358
|
+
response.Body.pipe(write_stream);
|
|
359
|
+
write_stream.on('finish', resolve);
|
|
360
|
+
write_stream.on('error', reject);
|
|
361
|
+
response.Body.on('error', reject);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// NOTE: Verify checksum if available in metadata.
|
|
365
|
+
if (response.Metadata && response.Metadata.checksum) {
|
|
366
|
+
const downloaded_checksum = await calculate_sha256(temp_backup_path);
|
|
367
|
+
if (downloaded_checksum !== response.Metadata.checksum) {
|
|
368
|
+
throw new Error(`Backup checksum mismatch. Expected: ${response.Metadata.checksum}, Got: ${downloaded_checksum}`);
|
|
369
|
+
}
|
|
370
|
+
log.info('Backup checksum verified', { checksum: downloaded_checksum });
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// NOTE: Stop database operations temporarily.
|
|
374
|
+
const db = get_database();
|
|
375
|
+
|
|
376
|
+
// NOTE: Create backup of current data.
|
|
377
|
+
const current_data_backup = resolve('./data_backup_before_restore');
|
|
378
|
+
if (existsSync('./data')) {
|
|
379
|
+
const backup_current_process = spawn('cp', ['-r', './data', current_data_backup]);
|
|
380
|
+
await new Promise((resolve) => {
|
|
381
|
+
backup_current_process.on('close', resolve);
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
// NOTE: Remove current data directory.
|
|
387
|
+
if (existsSync('./data')) {
|
|
388
|
+
await rm('./data', { recursive: true, force: true });
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// NOTE: Create new data directory.
|
|
392
|
+
await mkdir('./data', { recursive: true });
|
|
393
|
+
|
|
394
|
+
// NOTE: Extract backup.
|
|
395
|
+
const tar_process = spawn('tar', [
|
|
396
|
+
'-xzf',
|
|
397
|
+
temp_backup_path,
|
|
398
|
+
'-C',
|
|
399
|
+
'./data'
|
|
400
|
+
], {
|
|
401
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
let tar_error = '';
|
|
405
|
+
tar_process.stderr.on('data', (data) => {
|
|
406
|
+
tar_error += data.toString();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
const tar_exit_code = await new Promise((resolve) => {
|
|
410
|
+
tar_process.on('close', resolve);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
if (tar_exit_code !== 0) {
|
|
414
|
+
throw new Error(`Tar extraction failed with exit code ${tar_exit_code}: ${tar_error}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// NOTE: Clean up temporary files.
|
|
418
|
+
unlinkSync(temp_backup_path);
|
|
419
|
+
await rm(temp_dir, { recursive: true, force: true });
|
|
420
|
+
|
|
421
|
+
// NOTE: Clean up current data backup on success.
|
|
422
|
+
if (existsSync(current_data_backup)) {
|
|
423
|
+
await rm(current_data_backup, { recursive: true, force: true });
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const restore_duration = Date.now() - restore_start;
|
|
427
|
+
|
|
428
|
+
log.info('Backup restore completed successfully', {
|
|
429
|
+
backup_filename,
|
|
430
|
+
duration_ms: restore_duration
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
backup_filename,
|
|
435
|
+
restored_at: new Date().toISOString(),
|
|
436
|
+
duration_ms: restore_duration,
|
|
437
|
+
status: 'success'
|
|
438
|
+
};
|
|
439
|
+
} catch (restore_error) {
|
|
440
|
+
// NOTE: Restore original data on failure.
|
|
441
|
+
if (existsSync(current_data_backup)) {
|
|
442
|
+
if (existsSync('./data')) {
|
|
443
|
+
await rm('./data', { recursive: true, force: true });
|
|
444
|
+
}
|
|
445
|
+
const restore_original_process = spawn('mv', [current_data_backup, './data']);
|
|
446
|
+
await new Promise((resolve) => {
|
|
447
|
+
restore_original_process.on('close', resolve);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
throw restore_error;
|
|
451
|
+
}
|
|
452
|
+
} catch (error) {
|
|
453
|
+
log.error('Backup restore failed', { backup_filename, error: error.message });
|
|
454
|
+
|
|
455
|
+
// NOTE: Clean up on failure.
|
|
456
|
+
try {
|
|
457
|
+
const temp_dir = resolve('./temp_restore');
|
|
458
|
+
if (existsSync(temp_dir)) {
|
|
459
|
+
await rm(temp_dir, { recursive: true, force: true });
|
|
460
|
+
}
|
|
461
|
+
} catch (cleanup_error) {
|
|
462
|
+
log.warn('Failed to clean up temporary restore files', { error: cleanup_error.message });
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Cleans up old backups according to retention policy.
|
|
471
|
+
* Implements tiered retention: hourly backups for 24 hours, daily backups for 30 days,
|
|
472
|
+
* and deletes backups older than 30 days. Limits hourly backups to 24 most recent.
|
|
473
|
+
* @returns {Promise<Object>} Cleanup operation result
|
|
474
|
+
* @returns {number} returns.deleted_count - Number of backups deleted
|
|
475
|
+
* @returns {number} returns.retained_count - Number of backups retained
|
|
476
|
+
* @returns {number} returns.hourly_backups - Number of hourly backups retained
|
|
477
|
+
* @returns {number} returns.daily_backups - Number of daily backups retained
|
|
478
|
+
* @throws {Error} When backup listing or deletion operations fail
|
|
479
|
+
*/
|
|
480
|
+
const cleanup_old_backups = async () => {
|
|
481
|
+
const log = create_context_logger();
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
const { client, bucket } = get_s3_client();
|
|
485
|
+
|
|
486
|
+
const backups_list = await list_backups();
|
|
487
|
+
const backups = backups_list.backups;
|
|
488
|
+
|
|
489
|
+
if (backups.length === 0) {
|
|
490
|
+
return { deleted_count: 0, retained_count: 0 };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// NOTE: Sort by date (newest first).
|
|
494
|
+
backups.sort((a, b) => new Date(b.last_modified) - new Date(a.last_modified));
|
|
495
|
+
|
|
496
|
+
const now = new Date();
|
|
497
|
+
const one_hour = 60 * 60 * 1000;
|
|
498
|
+
const one_day = 24 * one_hour;
|
|
499
|
+
|
|
500
|
+
const hourly_backups = [];
|
|
501
|
+
const daily_backups = [];
|
|
502
|
+
const to_delete = [];
|
|
503
|
+
|
|
504
|
+
for (const backup of backups) {
|
|
505
|
+
const backup_date = new Date(backup.last_modified);
|
|
506
|
+
const age_ms = now - backup_date;
|
|
507
|
+
|
|
508
|
+
if (age_ms <= 24 * one_hour) {
|
|
509
|
+
// NOTE: Keep hourly backups for last 24 hours.
|
|
510
|
+
hourly_backups.push(backup);
|
|
511
|
+
} else if (age_ms <= 30 * one_day) {
|
|
512
|
+
// NOTE: Keep one backup per day for last 30 days.
|
|
513
|
+
const backup_day = backup_date.toDateString();
|
|
514
|
+
const existing_daily = daily_backups.find(b => new Date(b.last_modified).toDateString() === backup_day);
|
|
515
|
+
|
|
516
|
+
if (!existing_daily) {
|
|
517
|
+
daily_backups.push(backup);
|
|
518
|
+
} else {
|
|
519
|
+
to_delete.push(backup);
|
|
520
|
+
}
|
|
521
|
+
} else {
|
|
522
|
+
// NOTE: Delete backups older than 30 days.
|
|
523
|
+
to_delete.push(backup);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// NOTE: Limit hourly backups to 24.
|
|
528
|
+
if (hourly_backups.length > 24) {
|
|
529
|
+
to_delete.push(...hourly_backups.slice(24));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// NOTE: Delete old backups.
|
|
533
|
+
for (const backup of to_delete) {
|
|
534
|
+
try {
|
|
535
|
+
const delete_command = new DeleteObjectCommand({
|
|
536
|
+
Bucket: bucket,
|
|
537
|
+
Key: backup.filename
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
await client.send(delete_command);
|
|
541
|
+
log.info('Deleted old backup', { filename: backup.filename });
|
|
542
|
+
} catch (delete_error) {
|
|
543
|
+
log.warn('Failed to delete backup', {
|
|
544
|
+
filename: backup.filename,
|
|
545
|
+
error: delete_error.message
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const retained_count = backups.length - to_delete.length;
|
|
551
|
+
|
|
552
|
+
log.info('Backup cleanup completed', {
|
|
553
|
+
deleted_count: to_delete.length,
|
|
554
|
+
retained_count,
|
|
555
|
+
hourly_backups: Math.min(hourly_backups.length, 24),
|
|
556
|
+
daily_backups: daily_backups.length
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
deleted_count: to_delete.length,
|
|
561
|
+
retained_count,
|
|
562
|
+
hourly_backups: Math.min(hourly_backups.length, 24),
|
|
563
|
+
daily_backups: daily_backups.length
|
|
564
|
+
};
|
|
565
|
+
} catch (error) {
|
|
566
|
+
log.error('Backup cleanup failed', { error: error.message });
|
|
567
|
+
throw error;
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Starts automated backup scheduling based on configuration.
|
|
573
|
+
* Sets up periodic backup creation and cleanup according to the configured schedule
|
|
574
|
+
* (hourly, daily, or weekly). Handles invalid schedule configurations gracefully.
|
|
575
|
+
* @returns {void}
|
|
576
|
+
*/
|
|
577
|
+
const start_backup_schedule = () => {
|
|
578
|
+
const log = create_context_logger();
|
|
579
|
+
|
|
580
|
+
try {
|
|
581
|
+
const settings = get_settings();
|
|
582
|
+
|
|
583
|
+
if (!settings.s3) {
|
|
584
|
+
log.info('S3 not configured, backup scheduling disabled');
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const schedule = settings.backup_schedule || 'hourly';
|
|
589
|
+
|
|
590
|
+
let interval_ms;
|
|
591
|
+
switch (schedule) {
|
|
592
|
+
case 'hourly':
|
|
593
|
+
interval_ms = 60 * 60 * 1000; // 1 hour
|
|
594
|
+
break;
|
|
595
|
+
case 'daily':
|
|
596
|
+
interval_ms = 24 * 60 * 60 * 1000; // 24 hours
|
|
597
|
+
break;
|
|
598
|
+
case 'weekly':
|
|
599
|
+
interval_ms = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
600
|
+
break;
|
|
601
|
+
default:
|
|
602
|
+
log.warn('Invalid backup schedule, using hourly', { schedule });
|
|
603
|
+
interval_ms = 60 * 60 * 1000;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (backup_schedule_timer) {
|
|
607
|
+
clearInterval(backup_schedule_timer);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
backup_schedule_timer = setInterval(async () => {
|
|
611
|
+
try {
|
|
612
|
+
log.info('Starting scheduled backup', { schedule });
|
|
613
|
+
await create_backup();
|
|
614
|
+
await cleanup_old_backups();
|
|
615
|
+
} catch (error) {
|
|
616
|
+
log.error('Scheduled backup failed', { error: error.message });
|
|
617
|
+
}
|
|
618
|
+
}, interval_ms);
|
|
619
|
+
|
|
620
|
+
log.info('Backup schedule started', { schedule, interval_ms });
|
|
621
|
+
} catch (error) {
|
|
622
|
+
log.error('Failed to start backup schedule', { error: error.message });
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Stops the automated backup scheduling timer.
|
|
628
|
+
* Clears the active backup schedule interval and resets the timer reference.
|
|
629
|
+
* @returns {void}
|
|
630
|
+
*/
|
|
631
|
+
const stop_backup_schedule = () => {
|
|
632
|
+
const log = create_context_logger();
|
|
633
|
+
|
|
634
|
+
if (backup_schedule_timer) {
|
|
635
|
+
clearInterval(backup_schedule_timer);
|
|
636
|
+
backup_schedule_timer = null;
|
|
637
|
+
log.info('Backup schedule stopped');
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
export {
|
|
642
|
+
get_s3_client,
|
|
643
|
+
test_s3_connection,
|
|
644
|
+
create_backup,
|
|
645
|
+
list_backups,
|
|
646
|
+
restore_backup,
|
|
647
|
+
cleanup_old_backups,
|
|
648
|
+
start_backup_schedule,
|
|
649
|
+
stop_backup_schedule
|
|
650
|
+
};
|