Fresh Release v0.1.2

This commit is contained in:
Jyotirmoy Bandyopadhayaya 2023-05-08 00:55:03 +05:30
commit b6f82eb6a9
Signed by: bravo68web
GPG Key ID: F5671FD7BCB9917A
19 changed files with 1068 additions and 0 deletions

0
.eslintrc Normal file
View File

1
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1 @@
* @BRAVO68WEB

50
.github/workflows/build-image-push.yaml vendored Normal file
View File

@ -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

19
.github/workflows/testing.yaml vendored Normal file
View File

@ -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

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
dump.rdb
**/stockpile.dump
testBox.js
node_modules
*.dat
config.json
stockpile.json

3
.prettierrc Normal file
View File

@ -0,0 +1,3 @@
{
"tabWidth": 4
}

13
Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM node:alpine
WORKDIR /usr/app
COPY package.json .
RUN yarn
COPY . .
EXPOSE 6379
ENTRYPOINT ["node"]

21
LICENSE Normal file
View File

@ -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.

58
README.md Normal file
View File

@ -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.

102
app/bootstrap.js vendored Normal file
View File

@ -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");
}

36
app/buffer.js Normal file
View File

@ -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,
};

538
app/main.js Normal file
View File

@ -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);
});
};

11
codecrafters.yml Normal file
View File

@ -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

22
create-container.sh Executable file
View File

@ -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

BIN
db/stockpile.db Normal file

Binary file not shown.

42
package.json Normal file
View File

@ -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"
}
}

10
spawn_redis_server.sh Executable file
View File

@ -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

122
test_local.sh Executable file
View File

@ -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

13
yarn.lock Normal file
View File

@ -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==