Fresh Release v0.1.2
This commit is contained in:
commit
b6f82eb6a9
|
@ -0,0 +1 @@
|
|||
* @BRAVO68WEB
|
|
@ -0,0 +1,50 @@
|
|||
name: Build + Publish Docker Image to GHCR
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: ['master']
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: stockpile
|
||||
jobs:
|
||||
build-and-push-image:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
# Fetch the code
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Prepare
|
||||
id: prep
|
||||
run: echo ::set-output name=version::${GITHUB_REF##*/}
|
||||
|
||||
# Use QEMU for multi-architecture builds
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
with:
|
||||
platforms: all
|
||||
|
||||
# Login into GH container registry
|
||||
- name: Log in to registry
|
||||
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
|
||||
# Build the image from default Dockerfile
|
||||
- name: Build image
|
||||
run: docker build . --file Dockerfile --tag $IMAGE_NAME --label "runnumber=${GITHUB_RUN_ID}"
|
||||
|
||||
# Push build image
|
||||
- name: Push image
|
||||
run: |
|
||||
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
|
||||
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
|
||||
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
||||
[ "$VERSION" == "master" ] && VERSION=latest
|
||||
echo IMAGE_ID=$IMAGE_ID
|
||||
echo VERSION=$VERSION
|
||||
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
|
||||
docker push $IMAGE_ID:$VERSION
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
name: Build Checks
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16
|
||||
- run: npm install -g yarn
|
||||
- run: yarn
|
||||
- run: sudo apt install redis-tools
|
||||
- run: chmod +x spawn_redis_server.sh test_local.sh
|
||||
- run: ./spawn_redis_server.sh &
|
||||
- run: ./test_local.sh
|
|
@ -0,0 +1,7 @@
|
|||
dump.rdb
|
||||
**/stockpile.dump
|
||||
testBox.js
|
||||
node_modules
|
||||
*.dat
|
||||
config.json
|
||||
stockpile.json
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"tabWidth": 4
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
FROM node:alpine
|
||||
|
||||
WORKDIR /usr/app
|
||||
|
||||
COPY package.json .
|
||||
|
||||
RUN yarn
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 6379
|
||||
|
||||
ENTRYPOINT ["node"]
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 Jyotirmoy "BRAVO68WEB" Bandyopadhayaya <contact@b68.dev>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,58 @@
|
|||
# Welcome to stockpile 👋
|
||||
|
||||
[![Version](https://img.shields.io/npm/v/@bravo68web/stockpile.svg)](https://www.npmjs.com/package/stockpile)
|
||||
[![Documentation](https://img.shields.io/badge/documentation-yes-brightgreen.svg)](https://github.com/BRAVO68WEB/stockpile#readme)
|
||||
[![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/BRAVO68WEB/stockpile/graphs/commit-activity)
|
||||
[![License: MIT](https://img.shields.io/github/license/BRAVO68WEB/stockpile)](https://github.com/BRAVO68WEB/stockpile/blob/master/LICENSE)
|
||||
[![Twitter: BRAVO68WEB](https://img.shields.io/twitter/follow/BRAVO68WEB.svg?style=social)](https://twitter.com/BRAVO68WEB)
|
||||
|
||||
> A Tiny Redis Server ...
|
||||
|
||||
Stockpile is a Tiny Redis Server, created from scratch in Node.js with only few dependencies. It is a work in progress, and is not yet ready for production use. It was initially a part of [CodeCrafter's - Create Redis Challenge](https://app.codecrafters.io/courses/redis/). But, I decided to make it a standalone project. I will be adding more features to it, and will be using it in my future projects. I will also be adding more tests to it. If you want to contribute, please feel free to do so.
|
||||
|
||||
### 🏠 [Homepage](https://github.com/BRAVO68WEB/stockpile#readme)
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
npx @bravo68web/stockpile
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
git clone https://github.com/BRAVO68WEB/stockpile.git
|
||||
cd stockpile
|
||||
```
|
||||
|
||||
## Run tests
|
||||
|
||||
```sh
|
||||
node app/main.js &
|
||||
./test_local.sh
|
||||
```
|
||||
|
||||
## Author
|
||||
|
||||
👤 **Jyotirmoy <BRAVO68WEB> Bandyopadhayaya**
|
||||
|
||||
- Website: https://itsmebravo.dev
|
||||
- Twitter: [@BRAVO68WEB](https://twitter.com/BRAVO68WEB)
|
||||
- Github: [@BRAVO68WEB](https://github.com/BRAVO68WEB)
|
||||
- LinkedIn: [@BRAVO68WEB](https://linkedin.com/in/BRAVO68WEB)
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions, issues and feature requests are welcome!
|
||||
|
||||
Feel free to check [issues page](https://github.com/BRAVO68WEB/stockpile/issues).
|
||||
|
||||
## Show your support
|
||||
|
||||
Give a ⭐️ if this project helped you!
|
||||
|
||||
## 📝 License
|
||||
|
||||
Copyright © 2023 [Jyotirmoy "BRAVO68WEB" Bandyopadhayaya](https://github.com/BRAVO68WEB).
|
||||
|
||||
This project is [MIT](https://github.com/BRAVO68WEB/stockpile/blob/master/LICENSE) licensed.
|
|
@ -0,0 +1,102 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// This is the main entry point for the application.
|
||||
|
||||
// CLI arguments are passed to this file, and then passed to the main application.
|
||||
// Do not use any CLI framework here, as it will be difficult to maintain.
|
||||
|
||||
const commandName = process.argv[2];
|
||||
const commandArgs = process.argv.slice(3);
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const pkg = require("../package.json");
|
||||
|
||||
if (commandName === "help") {
|
||||
console.log("Stockpile CLI");
|
||||
console.log("Version " + pkg.version);
|
||||
console.log("");
|
||||
console.log("Commands:");
|
||||
console.log(
|
||||
" init --name <name> --configpath <path> --auth <password> --port <port> --dumppath <path> | Create a new config file"
|
||||
);
|
||||
console.log(" start --configpath <path> | Start Stockpile");
|
||||
console.log(" help Display this help message");
|
||||
console.log("");
|
||||
console.log("Github : ", pkg.repository.url.split("+")[1]);
|
||||
console.log("Author : ", pkg.author.name);
|
||||
process.exit(0);
|
||||
} else if (commandName === "init") {
|
||||
try {
|
||||
console.log("Initializing new Stockpile config file");
|
||||
|
||||
let config = {
|
||||
name: "",
|
||||
configpath: os.homedir() + "/.stockpile.config.json",
|
||||
auth: "",
|
||||
port: 6379,
|
||||
dumppath: os.homedir() + "/.stockpile.dump",
|
||||
};
|
||||
|
||||
if (commandArgs.length % 2 !== 0) {
|
||||
console.log("Invalid init syntax");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const i in commandArgs) {
|
||||
if (commandArgs[i] === "--name") {
|
||||
config.name = commandArgs[parseInt(i) + 1];
|
||||
} else if (commandArgs[i] === "--configpath") {
|
||||
config.configpath = commandArgs[parseInt(i) + 1];
|
||||
} else if (commandArgs[i] === "--auth") {
|
||||
config.auth = commandArgs[parseInt(i) + 1];
|
||||
} else if (commandArgs[i] === "--port") {
|
||||
config.port = commandArgs[parseInt(i) + 1];
|
||||
} else if (commandArgs[i] === "--dumppath") {
|
||||
config.dumppath = commandArgs[parseInt(i) + 1];
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.resolve(config.configpath),
|
||||
JSON.stringify(config)
|
||||
);
|
||||
console.log("Config file created at " + config.configpath);
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (commandName === "start") {
|
||||
console.log("Starting Stockpile");
|
||||
|
||||
let configpath = "";
|
||||
for (const i in commandArgs) {
|
||||
if (commandArgs[i] === "--configpath") {
|
||||
configpath = commandArgs[parseInt(i) + 1];
|
||||
}
|
||||
}
|
||||
if (configpath === "") {
|
||||
console.log("No config file specified");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(configpath)) {
|
||||
console.log("Config file does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
const config = JSON.parse(fs.readFileSync(configpath));
|
||||
|
||||
["name", "configpath", "port", "dumppath"].forEach((key) => {
|
||||
if (!config[key]) {
|
||||
console.log("Invalid config file");
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
const Stockpile = require("./main.js");
|
||||
Stockpile(config);
|
||||
} else {
|
||||
console.log("Invalid command");
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
const { BSON } = require("bson");
|
||||
|
||||
function createBinaryData(mapData) {
|
||||
const data = {};
|
||||
// Add each map in the array to the data object
|
||||
for (let i = 0; i < mapData.length; i++) {
|
||||
data[i] = Object.fromEntries(mapData[i]);
|
||||
}
|
||||
// Serialize the data object with BSON
|
||||
return BSON.serialize(data);
|
||||
}
|
||||
|
||||
function readBinaryData(buffer) {
|
||||
const data = BSON.deserialize(buffer);
|
||||
const mapData = [];
|
||||
|
||||
// Convert each object in the data object to a Map
|
||||
for (const key in data) {
|
||||
const obj = data[key];
|
||||
const map = new Map();
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (typeof value === "object") {
|
||||
map.set(key, value);
|
||||
} else {
|
||||
map.set(key, value);
|
||||
}
|
||||
}
|
||||
mapData.push(map);
|
||||
}
|
||||
|
||||
return mapData;
|
||||
}
|
||||
module.exports = {
|
||||
createBinaryData,
|
||||
readBinaryData,
|
||||
};
|
|
@ -0,0 +1,538 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const net = require("net");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { createBinaryData, readBinaryData } = require("./buffer");
|
||||
|
||||
module.exports = (config) => {
|
||||
const MAX_DATABASES = 16;
|
||||
const DEFAULT_DATABASE = 0;
|
||||
|
||||
const authConfig = {
|
||||
enabled: config.auth ? true : false,
|
||||
isAuthenticated: false,
|
||||
password: config.auth ? config.auth : "",
|
||||
};
|
||||
|
||||
let dataStore = new Array(MAX_DATABASES);
|
||||
for (let i = 0; i < MAX_DATABASES; i++) {
|
||||
dataStore[i] = new Map();
|
||||
}
|
||||
|
||||
if (fs.existsSync(path.join(config.dumppath))) {
|
||||
dataStore = fs.readFileSync(path.join(config.dumppath));
|
||||
dataStore = readBinaryData(dataStore);
|
||||
}
|
||||
|
||||
let currentDatabase = DEFAULT_DATABASE;
|
||||
|
||||
const parseIncomingData = (data) => {
|
||||
const lines = data.toString().split("\r\n");
|
||||
return lines;
|
||||
};
|
||||
|
||||
const assignHandler = (command, connection) => {
|
||||
command[2] = command[2].toUpperCase();
|
||||
if (
|
||||
authConfig.enabled &&
|
||||
!authConfig.isAuthenticated &&
|
||||
command[2] !== "AUTH"
|
||||
) {
|
||||
connection.write("-ERR NOAUTH Authentication required.\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (command[2] == "AUTH") {
|
||||
if (authConfig.enabled) {
|
||||
if (authConfig.isAuthenticated) {
|
||||
connection.write("-ERR Already authenticated\r\n");
|
||||
} else if (command[4] == authConfig.password) {
|
||||
authConfig.isAuthenticated = true;
|
||||
connection.write("+OK Auth Successfull\r\n");
|
||||
} else if (command[4] != authConfig.password) {
|
||||
connection.write("-ERR Invalid Password\r\n");
|
||||
}
|
||||
} else {
|
||||
connection.write("-ERR Authentication is not enabled\r\n");
|
||||
}
|
||||
} else if (command[2] == "PING") {
|
||||
connection.write("+PONG\r\n");
|
||||
} else if (command[2] == "SET") {
|
||||
dataStore[currentDatabase].set(command[4], command[6]);
|
||||
let expireTime = null;
|
||||
|
||||
for (let i = 8; i < command.length; i += 2) {
|
||||
if (command[i].toUpperCase() === "PX") {
|
||||
expireTime = Date.now() + parseInt(command[i + 2]);
|
||||
} else if (command[i].toUpperCase() === "EX") {
|
||||
expireTime = Date.now() + parseInt(command[i + 2]) * 1000;
|
||||
}
|
||||
}
|
||||
|
||||
if (expireTime) {
|
||||
dataStore[currentDatabase].set(
|
||||
command[4] + "_expire",
|
||||
expireTime
|
||||
);
|
||||
}
|
||||
connection.write("+OK\r\n");
|
||||
} else if (command[2] == "GET") {
|
||||
if (
|
||||
dataStore[currentDatabase].get(command[4] + "_expire") &&
|
||||
dataStore[currentDatabase].get(command[4] + "_expire") <
|
||||
Date.now()
|
||||
) {
|
||||
connection.write("$-1\r\n");
|
||||
dataStore[currentDatabase].delete(command[4]);
|
||||
dataStore[currentDatabase].delete(command[4] + "_expire");
|
||||
} else if (dataStore[currentDatabase].get(command[4]))
|
||||
connection.write(
|
||||
"+" + dataStore[currentDatabase].get(command[4]) + "\r\n"
|
||||
);
|
||||
else connection.write("$-1\r\n");
|
||||
} else if (command[2] == "DEL") {
|
||||
dataStore[currentDatabase].delete(command[4]);
|
||||
connection.write(":1\r\n");
|
||||
} else if (command[2] == "ECHO") {
|
||||
connection.write(
|
||||
"$" + command[4].length + "\r\n" + command[4] + "\r\n"
|
||||
);
|
||||
} else if (command[2] == "EXISTS") {
|
||||
if (dataStore[currentDatabase].has(command[4]))
|
||||
connection.write(":1\r\n");
|
||||
else connection.write(":0\r\n");
|
||||
} else if (command[2] == "KEYS") {
|
||||
let pattern = command[4];
|
||||
let keys = Array.from(dataStore[currentDatabase].keys());
|
||||
let matchingKeys = keys.filter((key) => {
|
||||
const regex = new RegExp(pattern.replace("*", ".*"));
|
||||
return regex.test(key);
|
||||
});
|
||||
let response = "*" + matchingKeys.length + "\r\n";
|
||||
for (let key of matchingKeys) {
|
||||
response += "$" + key.length + "\r\n" + key + "\r\n";
|
||||
}
|
||||
connection.write(response);
|
||||
} else if (command[2] == "APPEND") {
|
||||
let key = command[4];
|
||||
let value = command[6];
|
||||
if (dataStore[currentDatabase].has(key)) {
|
||||
let newValue = dataStore[currentDatabase].get(key) + value;
|
||||
dataStore[currentDatabase].set(key, newValue);
|
||||
connection.write(":" + newValue.length + "\r\n");
|
||||
} else {
|
||||
dataStore[currentDatabase].set(key, value);
|
||||
connection.write(":" + value.length + "\r\n");
|
||||
}
|
||||
} else if (command[2] == "STRLEN") {
|
||||
let key = command[4];
|
||||
if (dataStore[currentDatabase].has(key)) {
|
||||
let value = dataStore[currentDatabase].get(key);
|
||||
connection.write(":" + value.length + "\r\n");
|
||||
} else {
|
||||
connection.write(":0\r\n");
|
||||
}
|
||||
} else if (command[2] == "SETNX") {
|
||||
let key = command[4];
|
||||
let value = command[6];
|
||||
if (dataStore[currentDatabase].has(key)) {
|
||||
connection.write(":0\r\n");
|
||||
} else {
|
||||
dataStore[currentDatabase].set(key, value);
|
||||
connection.write(":1\r\n");
|
||||
}
|
||||
} else if (command[2] == "SETRANGE") {
|
||||
let key = command[4];
|
||||
let offset = command[6];
|
||||
let value = command[8];
|
||||
if (dataStore[currentDatabase].has(key)) {
|
||||
let oldValue = dataStore[currentDatabase].get(key);
|
||||
let newValue =
|
||||
oldValue.substring(0, offset) +
|
||||
value +
|
||||
oldValue.substring(offset + value.length);
|
||||
dataStore[currentDatabase].set(key, newValue);
|
||||
connection.write(":" + newValue.length + "\r\n");
|
||||
} else {
|
||||
dataStore[currentDatabase].set(key, value);
|
||||
connection.write(":" + value.length + "\r\n");
|
||||
}
|
||||
} else if (command[2] == "GETRANGE") {
|
||||
let key = command[4];
|
||||
let start = command[6];
|
||||
let end = command[8];
|
||||
if (dataStore[currentDatabase].has(key)) {
|
||||
let value = dataStore[currentDatabase].get(key);
|
||||
let newValue = value.substring(start, end + 1);
|
||||
connection.write(
|
||||
"$" + newValue.length + "\r\n" + newValue + "\r\n"
|
||||
);
|
||||
} else {
|
||||
connection.write("$-1\r\n");
|
||||
}
|
||||
} else if (command[2] == "MSET") {
|
||||
const keyValuePairs = command.slice(4);
|
||||
for (let i = 0; i < keyValuePairs.length; ) {
|
||||
dataStore[currentDatabase].set(
|
||||
keyValuePairs[i],
|
||||
keyValuePairs[i + 2]
|
||||
);
|
||||
i += 4;
|
||||
}
|
||||
connection.write("+OK\r\n");
|
||||
} else if (command[2] == "MGET") {
|
||||
const keys = command.slice(1);
|
||||
const values = keys.map(
|
||||
(key) => dataStore[currentDatabase].get(key) ?? null
|
||||
);
|
||||
|
||||
if (values.includes(null)) {
|
||||
connection.write("$-1\r\n");
|
||||
} else {
|
||||
const response = values.reduce((acc, value) => {
|
||||
return `${acc}\r\n$${value.length}\r\n${value}`;
|
||||
}, `*${values.length}`);
|
||||
connection.write(response);
|
||||
}
|
||||
} else if (command[2] == "FLUSHDB") {
|
||||
dataStore[currentDatabase].clear();
|
||||
connection.write("+OK\r\n");
|
||||
} else if (command[2] == "FLUSHALL") {
|
||||
dataStore = [];
|
||||
for (let i = 0; i < 16; i++) {
|
||||
dataStore.push(new Map());
|
||||
}
|
||||
connection.write("+OK\r\n");
|
||||
} else if (command[2] == "DBSIZE") {
|
||||
let size = dataStore[currentDatabase].size;
|
||||
connection.write(":" + size + "\r\n");
|
||||
} else if (command[2] == "SELECT") {
|
||||
let database = command[4];
|
||||
currentDatabase = database;
|
||||
connection.write("+OK\r\n");
|
||||
} else if (command[2] == "RANDOMKEY") {
|
||||
let keys = Array.from(dataStore[currentDatabase].keys());
|
||||
if (keys.length == 0) {
|
||||
connection.write("$-1\r\n");
|
||||
return;
|
||||
}
|
||||
let randomKey = keys[Math.floor(Math.random() * keys.length)];
|
||||
connection.write(
|
||||
"$" + randomKey.length + "\r\n" + randomKey + "\r\n"
|
||||
);
|
||||
} else if (command[2] == "INCR") {
|
||||
let key = command[4];
|
||||
if (dataStore[currentDatabase].has(key)) {
|
||||
let value = dataStore[currentDatabase].get(key);
|
||||
let newValue = parseInt(value) + 1;
|
||||
dataStore[currentDatabase].set(key, newValue.toString());
|
||||
connection.write(":" + newValue + "\r\n");
|
||||
} else {
|
||||
dataStore[currentDatabase].set(key, "1");
|
||||
connection.write(":1\r\n");
|
||||
}
|
||||
} else if (command[2] == "INCRBY") {
|
||||
let key = command[4];
|
||||
let increment = command[6];
|
||||
if (dataStore[currentDatabase].has(key)) {
|
||||
let value = dataStore[currentDatabase].get(key);
|
||||
let newValue = parseInt(value) + parseInt(increment);
|
||||
dataStore[currentDatabase].set(key, newValue.toString());
|
||||
connection.write(":" + newValue + "\r\n");
|
||||
} else {
|
||||
dataStore[currentDatabase].set(key, increment);
|
||||
connection.write(":" + increment + "\r\n");
|
||||
}
|
||||
} else if (command[2] == "DECR") {
|
||||
let key = command[4];
|
||||
if (dataStore[currentDatabase].has(key)) {
|
||||
let value = dataStore[currentDatabase].get(key);
|
||||
let newValue = parseInt(value) - 1;
|
||||
dataStore[currentDatabase].set(key, newValue.toString());
|
||||
connection.write(":" + newValue + "\r\n");
|
||||
} else {
|
||||
dataStore[currentDatabase].set(key, "-1");
|
||||
connection.write(":-1\r\n");
|
||||
}
|
||||
} else if (command[2] == "DECRBY") {
|
||||
let key = command[4];
|
||||
let decrement = command[6];
|
||||
if (dataStore[currentDatabase].has(key)) {
|
||||
let value = dataStore[currentDatabase].get(key);
|
||||
let newValue = parseInt(value) - parseInt(decrement);
|
||||
dataStore[currentDatabase].set(key, newValue.toString());
|
||||
connection.write(":" + newValue + "\r\n");
|
||||
} else {
|
||||
dataStore[currentDatabase].set(key, "-" + decrement);
|
||||
connection.write(":-" + decrement + "\r\n");
|
||||
}
|
||||
} else if (command[2] == "EXPIRE") {
|
||||
const key = command[4];
|
||||
const seconds = parseInt(command[6]);
|
||||
if (dataStore[currentDatabase].has(key)) {
|
||||
// Set the expiration time of the key
|
||||
const timestamp = Math.floor(Date.now() / 1000) + seconds;
|
||||
dataStore[currentDatabase].set(`${key}:expire`, timestamp);
|
||||
connection.write(":1\r\n");
|
||||
|
||||
// Schedule the key for deletion when the expiration time has elapsed
|
||||
setTimeout(() => {
|
||||
if (dataStore[currentDatabase].has(key)) {
|
||||
dataStore[currentDatabase].delete(key);
|
||||
dataStore[currentDatabase].delete(`${key}:expire`);
|
||||
}
|
||||
}, seconds * 1000);
|
||||
} else {
|
||||
connection.write(":0\r\n");
|
||||
}
|
||||
} else if (command[2] == "TTL") {
|
||||
const key = command[4];
|
||||
if (dataStore[currentDatabase].has(key)) {
|
||||
// Get the expiration time of the key
|
||||
const expireKey = `${key}:expire`;
|
||||
if (dataStore[currentDatabase].has(expireKey)) {
|
||||
const timestamp = dataStore[currentDatabase].get(expireKey);
|
||||
const ttl = Math.max(
|
||||
timestamp - Math.floor(Date.now() / 1000),
|
||||
0
|
||||
);
|
||||
connection.write(`:${ttl}\r\n`);
|
||||
} else {
|
||||
connection.write(":-1\r\n");
|
||||
}
|
||||
} else {
|
||||
connection.write(":-2\r\n");
|
||||
}
|
||||
} else if (command[2] == "EXPIREAT") {
|
||||
const key = command[4];
|
||||
const timestamp = parseInt(command[6]);
|
||||
if (dataStore[currentDatabase].has(key)) {
|
||||
// Set the expiration time of the key
|
||||
dataStore[currentDatabase].set(`${key}:expire`, timestamp);
|
||||
connection.write(":1\r\n");
|
||||
} else {
|
||||
connection.write(":0\r\n");
|
||||
}
|
||||
} else if (command[2] == "PERSIST") {
|
||||
const key = command[4];
|
||||
if (dataStore[currentDatabase].has(`${key}:expire`)) {
|
||||
dataStore[currentDatabase].delete(`${key}:expire`);
|
||||
connection.write(":1\r\n");
|
||||
} else {
|
||||
connection.write(":0\r\n");
|
||||
}
|
||||
} else if (command[2] == "HGET") {
|
||||
const key = command[4];
|
||||
const field = command[6];
|
||||
if (dataStore[currentDatabase].has(key)) {
|
||||
const value = dataStore[currentDatabase].get(key)[field];
|
||||
if (value !== undefined) {
|
||||
connection.write(`$${value.length}\r\n${value}\r\n`);
|
||||
} else {
|
||||
connection.write("$-1\r\n");
|
||||
}
|
||||
} else {
|
||||
connection.write("$-1\r\n");
|
||||
}
|
||||
} else if (command[2] == "HSET") {
|
||||
const key = command[4];
|
||||
const field = command[6];
|
||||
const value = command[8];
|
||||
if (!dataStore[currentDatabase].has(key)) {
|
||||
dataStore[currentDatabase].set(key, {});
|
||||
}
|
||||
dataStore[currentDatabase].get(key)[field] = value;
|
||||
connection.write(":1\r\n");
|
||||
} else if (command[2] == "HGETALL") {
|
||||
const key = command[4];
|
||||
if (dataStore[currentDatabase].has(key)) {
|
||||
const obj = dataStore[currentDatabase].get(key);
|
||||
let response = "*" + Object.keys(obj).length * 2 + "\r\n";
|
||||
for (const [field, value] of Object.entries(obj)) {
|
||||
response += `$${field.length}\r\n${field}\r\n`;
|
||||
response += `$${value.length}\r\n${value}\r\n`;
|
||||
}
|
||||
connection.write(response);
|
||||
} else {
|
||||
connection.write("*0\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
// HSETNX, HINCRBY, HDEL, HEXISTS, HKEYS, HLEN, HSTRLEN, HVALS
|
||||
else if (command[2] == "HSETNX") {
|
||||
const key = command[4];
|
||||
const field = command[6];
|
||||
const value = command[8];
|
||||
if (!dataStore[currentDatabase].has(key)) {
|
||||
dataStore[currentDatabase].set(key, {});
|
||||
}
|
||||
const obj = dataStore[currentDatabase].get(key);
|
||||
if (obj[field] === undefined) {
|
||||
obj[field] = value;
|
||||
connection.write(":1\r\n");
|
||||
} else {
|
||||
connection.write(":0\r\n");
|
||||
}
|
||||
} else if (command[2] == "HMSET") {
|
||||
const hash = command[1];
|
||||
const fieldsAndValues = command.slice(2);
|
||||
const hashExists = dataStore[currentDatabase].hasOwnProperty(hash);
|
||||
|
||||
if (!hashExists) {
|
||||
dataStore[currentDatabase][hash] = {};
|
||||
}
|
||||
|
||||
for (let i = 0; i < fieldsAndValues.length; i += 2) {
|
||||
const field = fieldsAndValues[i];
|
||||
const value = fieldsAndValues[i + 1];
|
||||
dataStore[currentDatabase][hash][field] = value;
|
||||
}
|
||||
|
||||
connection.write("+OK\r\n");
|
||||
} else if (command[2] == "HINCRBY") {
|
||||
const key = command[4];
|
||||
const field = command[6];
|
||||
const increment = Number(command[8]);
|
||||
if (!dataStore[currentDatabase].has(key)) {
|
||||
dataStore[currentDatabase].set(key, {});
|
||||
}
|
||||
const obj = dataStore[currentDatabase].get(key);
|
||||
if (obj[field] === undefined) {
|
||||
obj[field] = "0";
|
||||
}
|
||||
obj[field] = String(Number(obj[field]) + increment);
|
||||
connection.write(
|
||||
":" + obj[field].length + "\r\n" + obj[field] + "\r\n"
|
||||
);
|
||||
} else if (command[2] == "HDEL") {
|
||||
const key = command[4];
|
||||
const fields = command.slice(6);
|
||||
if (!dataStore[currentDatabase].has(key)) {
|
||||
connection.write(":0\r\n");
|
||||
} else {
|
||||
const obj = dataStore[currentDatabase].get(key);
|
||||
let count = 0;
|
||||
for (const field of fields) {
|
||||
if (obj.hasOwnProperty(field)) {
|
||||
delete obj[field];
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
if (Object.keys(obj).length === 0) {
|
||||
dataStore[currentDatabase].delete(key);
|
||||
}
|
||||
connection.write(":" + count + "\r\n");
|
||||
}
|
||||
} else if (command[2] == "HEXISTS") {
|
||||
const key = command[4];
|
||||
const field = command[6];
|
||||
if (!dataStore[currentDatabase].has(key)) {
|
||||
connection.write(":0\r\n");
|
||||
} else {
|
||||
const obj = dataStore[currentDatabase].get(key);
|
||||
if (obj.hasOwnProperty(field)) {
|
||||
connection.write(":1\r\n");
|
||||
} else {
|
||||
connection.write(":0\r\n");
|
||||
}
|
||||
}
|
||||
} else if (command[2] == "HKEYS") {
|
||||
const key = command[4];
|
||||
if (!dataStore[currentDatabase].has(key)) {
|
||||
connection.write("*0\r\n");
|
||||
} else {
|
||||
const obj = dataStore[currentDatabase].get(key);
|
||||
const fields = Object.keys(obj);
|
||||
let response = "*" + fields.length + "\r\n";
|
||||
for (const field of fields) {
|
||||
response += "$" + field.length + "\r\n" + field + "\r\n";
|
||||
}
|
||||
connection.write(response);
|
||||
}
|
||||
} else if (command[2] == "HLEN") {
|
||||
const key = command[4];
|
||||
if (!dataStore[currentDatabase].has(key)) {
|
||||
connection.write(":0\r\n");
|
||||
} else {
|
||||
const obj = dataStore[currentDatabase].get(key);
|
||||
const count = Object.keys(obj).length;
|
||||
connection.write(":" + count + "\r\n");
|
||||
}
|
||||
} else if (command[2] == "HSTRLEN") {
|
||||
const key = command[4];
|
||||
const field = command[6];
|
||||
if (!dataStore[currentDatabase].has(key)) {
|
||||
connection.write(":0\r\n");
|
||||
} else {
|
||||
const obj = dataStore[currentDatabase].get(key);
|
||||
if (obj.hasOwnProperty(field)) {
|
||||
const value = obj[field];
|
||||
connection.write(":" + value.length + "\r\n");
|
||||
} else {
|
||||
connection.write(":0\r\n");
|
||||
}
|
||||
}
|
||||
} else if (command[2] == "HVALS") {
|
||||
const key = command[4];
|
||||
if (!dataStore[currentDatabase].has(key)) {
|
||||
connection.write("*0\r\n");
|
||||
} else {
|
||||
const obj = dataStore[currentDatabase].get(key);
|
||||
const values = Object.values(obj);
|
||||
let response = "*" + values.length + "\r\n";
|
||||
for (const value of values) {
|
||||
response += "$" + value.length + "\r\n" + value + "\r\n";
|
||||
}
|
||||
connection.write(response);
|
||||
}
|
||||
} else if (command[2] == "DUMP") {
|
||||
saveDataToFile(dataStore);
|
||||
connection.write("+OK Saving Current State\r\n");
|
||||
} else if (command[0].includes("health")) {
|
||||
connection.write("OK !!\r\n");
|
||||
connection.end();
|
||||
} else if (command[0].includes("GET")) {
|
||||
connection.write("Stockpile is Running !!\r\n");
|
||||
connection.end();
|
||||
} else {
|
||||
connection.write("-ERR unknown command " + command[2] + "\r\n");
|
||||
}
|
||||
};
|
||||
|
||||
let id = 0;
|
||||
const server = net.createServer((connection) => {
|
||||
connection.id = id;
|
||||
id += 1;
|
||||
connection.on("data", (data) => {
|
||||
let command = parseIncomingData(data);
|
||||
assignHandler(command, connection);
|
||||
});
|
||||
|
||||
connection.on("end", (something) => {
|
||||
console.log("Client disconnected");
|
||||
});
|
||||
|
||||
connection.on("error", (err) => {
|
||||
console.log("Error in connection");
|
||||
console.log(err);
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(config.port, "0.0.0.0", () => {
|
||||
console.log("Starting server");
|
||||
console.log("Stockpile server listening on port 6379");
|
||||
});
|
||||
|
||||
function saveDataToFile(data, filename) {
|
||||
const binaryData = createBinaryData(data);
|
||||
fs.writeFileSync(config.dumppath, binaryData);
|
||||
}
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
console.log("\nTaking Snapshot !!");
|
||||
saveDataToFile(dataStore);
|
||||
console.log("Shutting down server");
|
||||
process.exit(0);
|
||||
});
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
# Set this to true if you want debug logs.
|
||||
#
|
||||
# These can be VERY verbose, so we suggest turning them off
|
||||
# unless you really need them.
|
||||
debug: true
|
||||
|
||||
# Use this to change the JavaScript version used to run your code
|
||||
# on Codecrafters.
|
||||
#
|
||||
# Available versions: nodejs-16
|
||||
language_pack: nodejs-16
|
|
@ -0,0 +1,22 @@
|
|||
#!/bin/bash
|
||||
|
||||
touch stockpile.json
|
||||
|
||||
echo "Do you want to enable authentication? (y/n)"
|
||||
read auth
|
||||
|
||||
echo "Enter desired port"
|
||||
read port
|
||||
|
||||
if([ $auth = "y" ])
|
||||
then
|
||||
echo "Enter password"
|
||||
read password
|
||||
docker run -v ./stockpile.json:/root/.stockpile.config.json stockpile app/bootstrap.js init --name "stockpile-db" --port 6379 --auth $password
|
||||
fi
|
||||
if([ $auth = "n" ])
|
||||
then
|
||||
docker run -v ./stockpile.json:/root/.stockpile.config.json stockpile app/bootstrap.js init --name "stockpile-db" --port 6379
|
||||
fi
|
||||
|
||||
docker run -d -p $port:6379 -v ./stockpile.json:/root/.stockpile.config.json -v ./db/stockpile.db:/root/.stockpile.dump stockpile app/bootstrap.js start --configpath /root/.stockpile.config.json
|
Binary file not shown.
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"name": "@bravo68web/stockpile",
|
||||
"version": "0.1.2",
|
||||
"description": "A Tiny Redis Server ...",
|
||||
"main": "app/main.js",
|
||||
"bin": {
|
||||
"stockpile": "app/main.js"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "nodemon app/main.js",
|
||||
"test": "./test_local.sh"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/BRAVO68WEB/stockpile.git"
|
||||
},
|
||||
"keywords": [
|
||||
"redis",
|
||||
"node-cache",
|
||||
"clone",
|
||||
"tiny",
|
||||
"self",
|
||||
"server",
|
||||
"service"
|
||||
],
|
||||
"author": {
|
||||
"email": "hi@b68.dev",
|
||||
"name": "Jyotirmoy <BRAVO68WEB> Bandyopadhayaya",
|
||||
"url": "https://itsmebravo.dev"
|
||||
},
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/BRAVO68WEB/stockpile/issues"
|
||||
},
|
||||
"homepage": "https://github.com/BRAVO68WEB/stockpile#readme",
|
||||
"dependencies": {
|
||||
"bson": "^5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^2.8.4"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# DON'T EDIT THIS!
|
||||
#
|
||||
# CodeCrafters uses this file to test your code. Don't make any changes here!
|
||||
#
|
||||
# DON'T EDIT THIS!
|
||||
currentLoc=$(pwd)
|
||||
node app/bootstrap.js init --name "stockpile-db" --port 6379 --configpath $currentLoc/stockpile.json --dumppath $currentLoc/stockpile.db
|
||||
exec node app/bootstrap.js start --configpath $currentLoc/stockpile.json
|
|
@ -0,0 +1,122 @@
|
|||
#!/bin/bash
|
||||
|
||||
echo "Test Redis PING command"
|
||||
redis-cli ping
|
||||
|
||||
echo "Test Redis SET and GET commands"
|
||||
redis-cli set name John
|
||||
redis-cli get name
|
||||
|
||||
echo "Test Redis DEL command"
|
||||
redis-cli del name
|
||||
redis-cli get name
|
||||
|
||||
echo "Test Redis ECHO command"
|
||||
redis-cli echo "Hello, Redis!"
|
||||
|
||||
echo "Test Redis EXISTS command"
|
||||
redis-cli set name John
|
||||
redis-cli exists name
|
||||
redis-cli del name
|
||||
redis-cli exists name
|
||||
|
||||
echo "Test Redis KEYS command"
|
||||
redis-cli set name John
|
||||
redis-cli set age 30
|
||||
redis-cli keys "*"
|
||||
|
||||
echo "Test Redis STRLEN command"
|
||||
redis-cli set message "Hello, Redis!"
|
||||
redis-cli strlen message
|
||||
|
||||
echo "Test Redis SETNX command"
|
||||
redis-cli setnx name John
|
||||
redis-cli setnx name Jane
|
||||
|
||||
echo "Test Redis SETRANGE and GETRANGE commands"
|
||||
redis-cli set greeting "Hello, World!"
|
||||
redis-cli setrange greeting 7 "Redis"
|
||||
redis-cli getrange greeting 0 11
|
||||
|
||||
echo "Test Redis MSET and MGET commands"
|
||||
redis-cli mset fruit1 apple fruit2 orange fruit3 banana
|
||||
redis-cli mget fruit1 fruit2 fruit3
|
||||
|
||||
echo "Test Redis FLUSHDB and FLUSHALL commands"
|
||||
redis-cli flushdb
|
||||
redis-cli set name John
|
||||
redis-cli flushall
|
||||
|
||||
echo "Test Redis DBSIZE command"
|
||||
redis-cli set fruit1 apple
|
||||
redis-cli set fruit2 orange
|
||||
redis-cli dbsize
|
||||
|
||||
echo "Test Redis SELECT command"
|
||||
redis-cli select 1
|
||||
redis-cli set fruit1 apple
|
||||
redis-cli get fruit1
|
||||
|
||||
echo "Test Redis RANDOMKEY command"
|
||||
redis-cli randomkey
|
||||
|
||||
echo "Test Redis INCR and DECR commands"
|
||||
redis-cli set count 10
|
||||
redis-cli incr count
|
||||
redis-cli decr count
|
||||
|
||||
echo "Test Redis INCRBY and DECRBY commands"
|
||||
redis-cli set count 10
|
||||
redis-cli incrby count 5
|
||||
redis-cli decrby count 3
|
||||
|
||||
echo "Test Redis EXPIRE, TTL, and EXPIREAT commands"
|
||||
redis-cli set name John
|
||||
redis-cli expire name 10
|
||||
redis-cli ttl name
|
||||
redis-cli expireat name "$(date +%s) + 10"
|
||||
redis-cli persist name
|
||||
|
||||
echo "Test Redis HGET, HSET, HGETALL, and HSETNX commands"
|
||||
redis-cli hmset person name John age 30
|
||||
redis-cli hget person name
|
||||
redis-cli hset person email john@example.com
|
||||
redis-cli hgetall person
|
||||
redis-cli hsetnx person name Jane
|
||||
|
||||
echo "Test Redis HINCRBY command"
|
||||
redis-cli hmset counter clicks 10 views 20
|
||||
redis-cli hincrby counter clicks 5
|
||||
redis-cli hget counter clicks
|
||||
|
||||
echo "Test Redis HDEL command"
|
||||
redis-cli hmset person name John age 30 email john@example.com
|
||||
redis-cli hdel person email
|
||||
redis-cli hgetall person
|
||||
|
||||
echo "Test Redis HEXISTS command"
|
||||
redis-cli hmset person name John age 30
|
||||
redis-cli hexists person name
|
||||
redis-cli hexists person email
|
||||
|
||||
echo "Test Redis HKEYS command"
|
||||
redis-cli hmset person name John age 30
|
||||
redis-cli hkeys person
|
||||
|
||||
echo "Test Redis HLEN command"
|
||||
redis-cli hmset person name John age 30
|
||||
redis-cli hlen person
|
||||
|
||||
echo "Test Redis HSTRLEN command"
|
||||
redis-cli hmset person name John age 30
|
||||
redis-cli hstrlen person name
|
||||
|
||||
echo "Test Redis HVALS command"
|
||||
redis-cli hmset person name John age 30
|
||||
redis-cli hvals person
|
||||
|
||||
echo "Dump current DB State"
|
||||
redis-cli dump
|
||||
|
||||
echo "Clean up by deleting all keys"
|
||||
redis-cli flushall
|
|
@ -0,0 +1,13 @@
|
|||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
bson@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/bson/-/bson-5.1.0.tgz#7b15cd9aa012b8bf9d320fbaefe15cc2fb657de2"
|
||||
integrity sha512-FEecNHkhYRBe7X9KDkdG12xNuz5VHGeH6mCE0B5sBmYtiR/Ux/9vUH/v4NUoBCDr6NuEhvahjoLiiRogptVW0A==
|
||||
|
||||
prettier@^2.8.4:
|
||||
version "2.8.4"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3"
|
||||
integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==
|
Loading…
Reference in New Issue