Compare commits

..

No commits in common. "old" and "main" have entirely different histories.
old ... main

56 changed files with 2215 additions and 3063 deletions

View file

@ -1,3 +0,0 @@
{
"extends": "next/core-web-vitals"
}

41
.gitignore vendored
View file

@ -1,38 +1,9 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.DS_STORE
# dependencies
/node_modules
/.pnp
.pnp.js
.env
**/*_templ.go
**/*_templ.txt
# testing
/coverage
**/configmap.yaml
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
# typescript
*.tsbuildinfo
tailwindcss

View file

@ -1,11 +1,13 @@
This is a code sharing web application written in next.js using Serverless Functions and MongoDB/FaunaDB as database.
# fastbin: a text sharing application
## Getting Started
https://fastbin.lab.divyam.dev
First, run the development server:
A text sharing application made with microservice architecture in golang.
Services:
- API Server (Read and Write)
- Custom Key Generator Service
- Database Service
```bash
npm run dev
# or
yarn dev
```
To-do:
- Add data deletion service which removes data after certain time.
- Make key generator service faster and scalable with bloom filter and redis caching.

169
api/keygen/keygen.pb.go Normal file
View file

@ -0,0 +1,169 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.35.1
// protoc v5.28.2
// source: keygen.proto
package keygen
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Empty struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *Empty) Reset() {
*x = Empty{}
mi := &file_keygen_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Empty) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Empty) ProtoMessage() {}
func (x *Empty) ProtoReflect() protoreflect.Message {
mi := &file_keygen_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Empty.ProtoReflect.Descriptor instead.
func (*Empty) Descriptor() ([]byte, []int) {
return file_keygen_proto_rawDescGZIP(), []int{0}
}
type Key struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"`
}
func (x *Key) Reset() {
*x = Key{}
mi := &file_keygen_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Key) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Key) ProtoMessage() {}
func (x *Key) ProtoReflect() protoreflect.Message {
mi := &file_keygen_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Key.ProtoReflect.Descriptor instead.
func (*Key) Descriptor() ([]byte, []int) {
return file_keygen_proto_rawDescGZIP(), []int{1}
}
func (x *Key) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
var File_keygen_proto protoreflect.FileDescriptor
var file_keygen_proto_rawDesc = []byte{
0x0a, 0x0c, 0x6b, 0x65, 0x79, 0x67, 0x65, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06,
0x6b, 0x65, 0x79, 0x67, 0x65, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22,
0x1b, 0x0a, 0x03, 0x4b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x32, 0x35, 0x0a, 0x06,
0x4b, 0x65, 0x79, 0x67, 0x65, 0x6e, 0x12, 0x2b, 0x0a, 0x0b, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61,
0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x0d, 0x2e, 0x6b, 0x65, 0x79, 0x67, 0x65, 0x6e, 0x2e, 0x45,
0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x6b, 0x65, 0x79, 0x67, 0x65, 0x6e, 0x2e, 0x4b, 0x65,
0x79, 0x22, 0x00, 0x42, 0x19, 0x5a, 0x17, 0x66, 0x61, 0x73, 0x74, 0x62, 0x69, 0x6e, 0x2f, 0x69,
0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6b, 0x65, 0x79, 0x67, 0x65, 0x6e, 0x62, 0x06,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_keygen_proto_rawDescOnce sync.Once
file_keygen_proto_rawDescData = file_keygen_proto_rawDesc
)
func file_keygen_proto_rawDescGZIP() []byte {
file_keygen_proto_rawDescOnce.Do(func() {
file_keygen_proto_rawDescData = protoimpl.X.CompressGZIP(file_keygen_proto_rawDescData)
})
return file_keygen_proto_rawDescData
}
var file_keygen_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_keygen_proto_goTypes = []any{
(*Empty)(nil), // 0: keygen.Empty
(*Key)(nil), // 1: keygen.Key
}
var file_keygen_proto_depIdxs = []int32{
0, // 0: keygen.Keygen.GenerateKey:input_type -> keygen.Empty
1, // 1: keygen.Keygen.GenerateKey:output_type -> keygen.Key
1, // [1:2] is the sub-list for method output_type
0, // [0:1] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_keygen_proto_init() }
func file_keygen_proto_init() {
if File_keygen_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_keygen_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_keygen_proto_goTypes,
DependencyIndexes: file_keygen_proto_depIdxs,
MessageInfos: file_keygen_proto_msgTypes,
}.Build()
File_keygen_proto = out.File
file_keygen_proto_rawDesc = nil
file_keygen_proto_goTypes = nil
file_keygen_proto_depIdxs = nil
}

15
api/keygen/keygen.proto Normal file
View file

@ -0,0 +1,15 @@
syntax = "proto3";
option go_package = "fastbin/internal/keygen";
package keygen;
service Keygen {
rpc GenerateKey(Empty) returns (Key) {}
}
message Empty {}
message Key {
string value = 1;
}

View file

@ -0,0 +1,121 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v5.28.2
// source: keygen.proto
package keygen
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
Keygen_GenerateKey_FullMethodName = "/keygen.Keygen/GenerateKey"
)
// KeygenClient is the client API for Keygen service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type KeygenClient interface {
GenerateKey(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Key, error)
}
type keygenClient struct {
cc grpc.ClientConnInterface
}
func NewKeygenClient(cc grpc.ClientConnInterface) KeygenClient {
return &keygenClient{cc}
}
func (c *keygenClient) GenerateKey(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Key, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Key)
err := c.cc.Invoke(ctx, Keygen_GenerateKey_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// KeygenServer is the server API for Keygen service.
// All implementations must embed UnimplementedKeygenServer
// for forward compatibility.
type KeygenServer interface {
GenerateKey(context.Context, *Empty) (*Key, error)
mustEmbedUnimplementedKeygenServer()
}
// UnimplementedKeygenServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedKeygenServer struct{}
func (UnimplementedKeygenServer) GenerateKey(context.Context, *Empty) (*Key, error) {
return nil, status.Errorf(codes.Unimplemented, "method GenerateKey not implemented")
}
func (UnimplementedKeygenServer) mustEmbedUnimplementedKeygenServer() {}
func (UnimplementedKeygenServer) testEmbeddedByValue() {}
// UnsafeKeygenServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to KeygenServer will
// result in compilation errors.
type UnsafeKeygenServer interface {
mustEmbedUnimplementedKeygenServer()
}
func RegisterKeygenServer(s grpc.ServiceRegistrar, srv KeygenServer) {
// If the following call pancis, it indicates UnimplementedKeygenServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&Keygen_ServiceDesc, srv)
}
func _Keygen_GenerateKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(KeygenServer).GenerateKey(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Keygen_GenerateKey_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(KeygenServer).GenerateKey(ctx, req.(*Empty))
}
return interceptor(ctx, in, info, handler)
}
// Keygen_ServiceDesc is the grpc.ServiceDesc for Keygen service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Keygen_ServiceDesc = grpc.ServiceDesc{
ServiceName: "keygen.Keygen",
HandlerType: (*KeygenServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GenerateKey",
Handler: _Keygen_GenerateKey_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "keygen.proto",
}

BIN
bin/web Executable file

Binary file not shown.

View file

@ -0,0 +1,22 @@
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o api-server ./cmd/api-server
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/api-server /app/api-server
ENV GIN_MODE=release
EXPOSE 8080
ENTRYPOINT ["/app/api-server"]

View file

@ -0,0 +1,43 @@
services:
psql_bp:
image: postgres:latest
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_DATABASE}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U", $DB_USERNAME, "-d", $DB_DATABASE]
interval: 5s
timeout: 5s
retries: 5
volumes:
- psql_volume_bp:/var/lib/postgresql/data
api_server:
env_file:
- ../../.env
depends_on:
psql_bp:
condition: service_healthy
keygen:
condition: service_started
build:
context: ../../
dockerfile: build/docker/api-server/Dockerfile
ports:
- 8080:8080
keygen:
build:
context: ../../
dockerfile: build/docker/keygen/Dockerfile
web_server:
env_file:
- ../../.env
build:
context: ../../
dockerfile: build/docker/api-server/Dockerfile
ports:
- 8080:8080
volumes:
psql_volume_bp:

View file

@ -0,0 +1,20 @@
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o keygen ./cmd/keygen
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/keygen /app/keygen
EXPOSE 8080
ENTRYPOINT ["/app/keygen"]

View file

@ -0,0 +1,36 @@
FROM golang:1.23-alpine AS builder
WORKDIR /app
RUN apk --no-cache add curl nodejs npm
RUN npm install -D tailwindcss@3
RUN go install github.com/a-h/templ/cmd/templ@latest
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go get github.com/a-h/templ@latest
RUN go mod tidy
RUN templ generate
RUN npx tailwindcss -i ./internal/web/assets/css/input.css -o ./internal/web/assets/css/output.css
RUN go build -o web ./cmd/web
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/web /app/web
ENV GIN_MODE=release
EXPOSE 8080
ENTRYPOINT ["/app/web"]

View file

@ -0,0 +1,67 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
spec:
replicas: 3
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
spec:
initContainers:
- name: wait-for-psql
image: busybox
command: ['sh', '-c', 'until nc -z psql-service 5432; do echo waiting for postgres readiness; sleep 2; done;']
containers:
- name: api-server
image: registry.lab.divyam.dev/fastbin-api-server:latest
ports:
- containerPort: 8080
env:
- name: DB_DATABASE
valueFrom:
configMapKeyRef:
name: fastbin-config
key: DB_DATABASE
- name: DB_USERNAME
valueFrom:
configMapKeyRef:
name: fastbin-config
key: DB_USERNAME
- name: DB_PASSWORD
valueFrom:
configMapKeyRef:
name: fastbin-config
key: DB_PASSWORD
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: fastbin-config
key: DB_HOST
- name: DB_PORT
valueFrom:
configMapKeyRef:
name: fastbin-config
key: DB_PORT
- name: KEYGEN_HOST
valueFrom:
configMapKeyRef:
name: fastbin-config
key: KEYGEN_HOST
---
apiVersion: v1
kind: Service
metadata:
name: api-server-service
spec:
ports:
- port: 80
targetPort: 8080
selector:
app: api-server
type: LoadBalancer

View file

@ -0,0 +1,12 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: fastbin-config
data:
DB_DATABASE: "fastbin"
DB_USERNAME: "username"
DB_PASSWORD: "password"
DB_HOST: "psql-service"
DB_PORT: "5432"
KEYGEN_HOST: "keygen-service"
API_URL: "http://api-server-service"

View file

@ -0,0 +1,30 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: keygen
spec:
replicas: 1
selector:
matchLabels:
app: keygen
template:
metadata:
labels:
app: keygen
spec:
containers:
- name: keygen
image: registry.lab.divyam.dev/fastbin-keygen-service:latest
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: keygen-service
spec:
ports:
- port: 8080
targetPort: 8080
selector:
app: keygen

View file

@ -0,0 +1,90 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: psql-pv
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
hostPath:
path: /mnt/data/postgres
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: psql-volume-claim
labels:
type: local
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: psql-bp
spec:
replicas: 1
selector:
matchLabels:
app: psql-bp
template:
metadata:
labels:
app: psql-bp
spec:
containers:
- name: postgres
image: postgres:latest
env:
- name: POSTGRES_DB
valueFrom:
configMapKeyRef:
name: fastbin-config
key: DB_DATABASE
- name: POSTGRES_USER
valueFrom:
configMapKeyRef:
name: fastbin-config
key: DB_USERNAME
- name: POSTGRES_PASSWORD
valueFrom:
configMapKeyRef:
name: fastbin-config
key: DB_PASSWORD
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: psql-storage
readinessProbe:
exec:
command:
- sh
- "-c"
- "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 5
volumes:
- name: psql-storage
persistentVolumeClaim:
claimName: psql-volume-claim
---
apiVersion: v1
kind: Service
metadata:
name: psql-service
spec:
ports:
- port: 5432
targetPort: 5432
selector:
app: psql-bp

View file

@ -0,0 +1,38 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: fastbin-web
spec:
replicas: 3
selector:
matchLabels:
app: fastbin-web
template:
metadata:
labels:
app: fastbin-web
spec:
containers:
- name: api-server
image: registry.lab.divyam.dev/fastbin-web:latest
ports:
- containerPort: 8080
env:
- name: API_URL
valueFrom:
configMapKeyRef:
name: fastbin-config
key: API_URL
---
apiVersion: v1
kind: Service
metadata:
name: fastbin-web-service
spec:
ports:
- port: 80
targetPort: 8080
selector:
app: fastbin-web
type: LoadBalancer

22
cmd/api-server/main.go Normal file
View file

@ -0,0 +1,22 @@
package main
import (
apiserver "fastbin/internal/api-server"
"fastbin/internal/pkg/env"
"fmt"
"log"
"net/http"
"strconv"
)
func main() {
port, err := strconv.Atoi(env.GetEnv("API_INTERNAL_PORT", "8080"))
if err != nil {
log.Fatalf("error listening port: %v, err: %v", port, err)
}
server := apiserver.NewAPIServer(port)
err = server.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
panic(fmt.Sprintf("http server error: %s", err))
}
}

25
cmd/keygen/main.go Normal file
View file

@ -0,0 +1,25 @@
package main
import (
keygen "fastbin/internal/keygen"
"fastbin/internal/pkg/env"
"fmt"
"log"
"net"
"strconv"
)
func main() {
port, err := strconv.Atoi(env.GetEnv("KEYGEN_INTERNAL_PORT", "8080"))
if err != nil {
log.Fatalf("error listening port: %v, err: %v", port, err)
}
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
err = keygen.NewKeygenServer().Serve(lis)
if err != nil {
log.Fatalf("grpc server error: %v", err)
}
}

22
cmd/web/main.go Normal file
View file

@ -0,0 +1,22 @@
package main
import (
"fastbin/internal/pkg/env"
webserver "fastbin/internal/web"
"fmt"
"log"
"net/http"
"strconv"
)
func main() {
port, err := strconv.Atoi(env.GetEnv("WEB_INTERNAL_PORT", "8080"))
if err != nil {
log.Fatalf("error listening port: %v, err: %v", port, err)
}
server := webserver.NewServer(port)
err = server.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
panic(fmt.Sprintf("http server error: %s", err))
}
}

51
go.mod Normal file
View file

@ -0,0 +1,51 @@
module fastbin
go 1.23.2
require (
github.com/a-h/templ v0.2.793
github.com/gin-gonic/gin v1.10.0
github.com/joho/godotenv v1.5.1
google.golang.org/grpc v1.67.1
google.golang.org/protobuf v1.34.2
gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.12
)
require (
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/rogpeppe/go-internal v1.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

126
go.sum Normal file
View file

@ -0,0 +1,126 @@
github.com/a-h/templ v0.2.793 h1:Io+/ocnfGWYO4VHdR0zBbf39PQlnzVCVVD+wEEs6/qY=
github.com/a-h/templ v0.2.793/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View file

@ -0,0 +1,128 @@
package apiserver
import (
"context"
"fmt"
"log"
"net/http"
"os"
"time"
pb "fastbin/api/keygen"
"fastbin/internal/pkg/env"
paste "fastbin/internal/pkg/paste"
"github.com/gin-gonic/gin"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
type Paste = paste.Paste
type APIServer struct {
db *gorm.DB
}
var (
database = os.Getenv("DB_DATABASE")
password = os.Getenv("DB_PASSWORD")
username = os.Getenv("DB_USERNAME")
dbport = os.Getenv("DB_PORT")
host = os.Getenv("DB_HOST")
)
func NewAPIServer(port int) *http.Server {
dbString := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable", host, username, password, database, dbport)
db, err := gorm.Open(postgres.Open(dbString), &gorm.Config{})
if err != nil {
log.Fatalf("failed to connect to db: %v", err)
}
db.AutoMigrate(&Paste{})
s := APIServer{db: db}
r := gin.Default()
r.POST("/write", s.write)
r.GET("/read/:key", s.read)
server := &http.Server{
Addr: fmt.Sprintf(":%d", port),
Handler: r,
}
return server
}
func (as *APIServer) read(gc *gin.Context) {
key, _ := gc.Params.Get("key")
var paste Paste
res := as.db.First(&paste, "id = ?", key)
if res.Error != nil {
gc.JSON(http.StatusNotFound, gin.H{
"error": "Not Found.",
"text": "",
})
} else {
gc.JSON(http.StatusOK, gin.H{
"text": paste.Text,
})
}
}
type WriteRequestBody struct {
Text string
}
func (as *APIServer) write(gc *gin.Context) {
var requestBody WriteRequestBody
if err := gc.BindJSON(&requestBody); err != nil {
fmt.Println(err)
gc.JSON(http.StatusBadRequest, gin.H{
"key": "",
})
}
key, err := as.try_write(requestBody.Text)
for tries := 0; err != nil && tries < 5; tries++ {
key, err = as.try_write(requestBody.Text)
}
if err != nil {
gc.JSON(http.StatusInternalServerError, gin.H{
"key": "",
})
} else {
gc.JSON(http.StatusOK, gin.H{
"key": key,
})
}
}
func (as *APIServer) try_write(data string) (string, error) {
grpcServerURL := env.GetEnv("KEYGEN_HOST", "localhost") + ":" + env.GetEnv("KEYGEN_PORT", "8080")
conn, err := grpc.NewClient(grpcServerURL, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return "", err
}
defer conn.Close()
c := pb.NewKeygenClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
r, err := c.GenerateKey(ctx, &pb.Empty{})
cancel()
if err != nil {
return "", err
}
key := r.Value
res := as.db.Create(&Paste{ID: key, Text: data})
if res.Error != nil {
return "", res.Error
}
return key, nil
}

30
internal/keygen/server.go Normal file
View file

@ -0,0 +1,30 @@
package keygen
import (
context "context"
"math/rand"
pb "fastbin/api/keygen"
"google.golang.org/grpc"
)
type keygenServer struct {
pb.UnimplementedKeygenServer
}
func (k *keygenServer) GenerateKey(ctx context.Context, req *pb.Empty) (*pb.Key, error) {
b := make([]byte, 6)
for i := range b {
b[i] = 'a' + byte(rand.Intn(26))
}
key := pb.Key{Value: string(b)}
return &key, nil
}
func NewKeygenServer() *grpc.Server {
grpcSever := grpc.NewServer()
pb.RegisterKeygenServer(grpcSever, &keygenServer{})
return grpcSever
}

28
internal/pkg/env/env.go vendored Normal file
View file

@ -0,0 +1,28 @@
package env
import (
"os"
"github.com/joho/godotenv"
)
func init() {
env := os.Getenv("FASTBIN_ENV")
if env == "" {
env = "development"
}
godotenv.Load(".env." + env + ".local")
if env != "test" {
godotenv.Load(".env.local")
}
godotenv.Load(".env." + env)
godotenv.Load()
}
func GetEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}

View file

@ -0,0 +1,11 @@
package paste
import (
"gorm.io/gorm"
)
type Paste struct {
gorm.Model
ID string `gorm:"primaryKey"`
Text string
}

View file

@ -0,0 +1,10 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--primary-color: 29 29 29;
--border-color: 69 69 69;
}
}

View file

@ -0,0 +1,740 @@
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
/*
! tailwindcss v3.4.14 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
7. Disable tap highlights on iOS
*/
html,
:host {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
font-variation-settings: normal;
/* 6 */
-webkit-tap-highlight-color: transparent;
/* 7 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font-family by default.
2. Use the user's configured `mono` font-feature-settings by default.
3. Use the user's configured `mono` font-variation-settings by default.
4. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-feature-settings: normal;
/* 2 */
font-variation-settings: normal;
/* 3 */
font-size: 1em;
/* 4 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-feature-settings: inherit;
/* 1 */
font-variation-settings: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
letter-spacing: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
input:where([type='button']),
input:where([type='reset']),
input:where([type='submit']) {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Reset default styling for dialogs.
*/
dialog {
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden]:where(:not([hidden="until-found"])) {
display: none;
}
:root {
--primary-color: 29 29 29;
--border-color: 69 69 69;
}
.pointer-events-none {
pointer-events: none;
}
.block {
display: block;
}
.inline-block {
display: inline-block;
}
.flex {
display: flex;
}
.h-\[70px\] {
height: 70px;
}
.h-dvh {
height: 100dvh;
}
.h-full {
height: 100%;
}
.w-\[70px\] {
width: 70px;
}
.w-dvw {
width: 100dvw;
}
.w-full {
width: 100%;
}
.flex-1 {
flex: 1 1 0%;
}
.cursor-pointer {
cursor: pointer;
}
.select-none {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.resize-none {
resize: none;
}
.resize {
resize: both;
}
.flex-col {
flex-direction: column;
}
.items-start {
align-items: flex-start;
}
.items-center {
align-items: center;
}
.items-stretch {
align-items: stretch;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.self-stretch {
align-self: stretch;
}
.border-b-2 {
border-bottom-width: 2px;
}
.border-none {
border-style: none;
}
.border-b-border {
--tw-border-opacity: 1;
border-bottom-color: rgb(var(--border-color) / var(--tw-border-opacity));
}
.bg-primary {
--tw-bg-opacity: 1;
background-color: rgb(var(--primary-color) / var(--tw-bg-opacity));
}
.bg-transparent {
background-color: transparent;
}
.px-2\.5 {
padding-left: 0.625rem;
padding-right: 0.625rem;
}
.px-\[20px\] {
padding-left: 20px;
padding-right: 20px;
}
.py-\[10px\] {
padding-top: 10px;
padding-bottom: 10px;
}
.text-center {
text-align: center;
}
.align-middle {
vertical-align: middle;
}
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.text-8xl {
font-size: 6rem;
line-height: 1;
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
.italic {
font-style: italic;
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.outline-none {
outline: 2px solid transparent;
outline-offset: 2px;
}
.filter {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.transition {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.hover\:bg-border\/50:hover {
background-color: rgb(var(--border-color) / 0.5);
}

View file

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

1
internal/web/assets/js/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

15
internal/web/efs.go Normal file
View file

@ -0,0 +1,15 @@
package web
import (
"embed"
"io/fs"
)
//go:embed assets
var f embed.FS
var Files fs.FS
func init() {
Files, _ = fs.Sub(f, "assets")
}

61
internal/web/renderer.go Normal file
View file

@ -0,0 +1,61 @@
// Credits: https://github.com/a-h/templ/blob/main/examples/integration-gin/gintemplrenderer/renderer.go
package web
import (
"context"
"net/http"
"github.com/gin-gonic/gin/render"
"github.com/a-h/templ"
)
var Default = &HTMLTemplRenderer{}
type HTMLTemplRenderer struct {
FallbackHtmlRenderer render.HTMLRender
}
func (r *HTMLTemplRenderer) Instance(s string, d any) render.Render {
templData, ok := d.(templ.Component)
if !ok {
if r.FallbackHtmlRenderer != nil {
return r.FallbackHtmlRenderer.Instance(s, d)
}
}
return &Renderer{
Ctx: context.Background(),
Status: -1,
Component: templData,
}
}
func NewGinTemplRenderer(ctx context.Context, status int, component templ.Component) *Renderer {
return &Renderer{
Ctx: ctx,
Status: status,
Component: component,
}
}
type Renderer struct {
Ctx context.Context
Status int
Component templ.Component
}
func (t Renderer) Render(w http.ResponseWriter) error {
t.WriteContentType(w)
if t.Status != -1 {
w.WriteHeader(t.Status)
}
if t.Component != nil {
return t.Component.Render(t.Ctx, w)
}
return nil
}
func (t Renderer) WriteContentType(w http.ResponseWriter) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
}

110
internal/web/server.go Normal file
View file

@ -0,0 +1,110 @@
package web
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"fastbin/internal/pkg/env"
"fastbin/internal/web/views"
)
type api_write_response struct {
Key string `json:"key"`
}
type api_read_response struct {
Error string `json:"error"`
Text string `json:"text"`
}
func NewServer(port int) *http.Server {
API_URL := env.GetEnv("API_URL", "localhost:8080")
r := gin.Default()
engineHTMLRenderer := r.HTMLRender
r.HTMLRender = &HTMLTemplRenderer{FallbackHtmlRenderer: engineHTMLRenderer}
r.StaticFS("/assets", http.FS(Files))
r.GET("/", func(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "", views.Write())
})
r.POST("/", func(ctx *gin.Context) {
text := ctx.Request.FormValue("text")
postBody, _ := json.Marshal(map[string]string{
"text": text,
})
url := API_URL + "/write"
response, err := http.Post(url, "application/json", bytes.NewBuffer(postBody))
if err != nil {
ctx.Redirect(http.StatusTemporaryRedirect, "/500")
}
var api_res api_write_response
decoder := json.NewDecoder(response.Body)
err = decoder.Decode(&api_res)
if err != nil {
ctx.Redirect(http.StatusTemporaryRedirect, "/500")
}
if response.StatusCode == http.StatusOK {
ctx.Writer.Header().Add("Hx-Redirect", api_res.Key)
} else {
ctx.Redirect(http.StatusTemporaryRedirect, "/500")
}
})
r.GET("/404", func(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "", views.NotFound())
})
r.GET("/500", func(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "", views.ServerError())
})
r.GET("/:key", func(ctx *gin.Context) {
url := API_URL + "/read/" + ctx.Param("key")
response, err := http.Get(url)
if err != nil {
ctx.Redirect(http.StatusTemporaryRedirect, "/500")
}
var api_res api_read_response
decoder := json.NewDecoder(response.Body)
err = decoder.Decode(&api_res)
if err != nil {
ctx.Redirect(http.StatusTemporaryRedirect, "/500")
}
if response.StatusCode == http.StatusOK {
ctx.HTML(http.StatusOK, "", views.Read(api_res.Text))
} else if response.StatusCode == http.StatusNotFound {
ctx.Redirect(http.StatusTemporaryRedirect, "/404")
} else {
ctx.Redirect(http.StatusTemporaryRedirect, "/500")
}
})
server := &http.Server{
Addr: fmt.Sprintf(":%d", port),
Handler: r,
IdleTimeout: time.Minute,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
}
return server
}

View file

@ -0,0 +1,17 @@
package views
templ NotFound() {
@Base("fastbin") {
@Header() {
<a href="/" class="inline-block text-center align-middle">
@Button() {
<span class="material-symbols-outlined">note_add</span>
}
</a>
}
<div class="flex flex-1 w-full text-white text-xl">
<div class="h-full px-2.5 select-none">></div>
<div class="flex w-full h-full bg-transparent resize-none text-white justify-center items-center text-8xl"><div>404: Page Not Found. </div></div>
</div>
}
}

View file

@ -0,0 +1,17 @@
package views
templ ServerError() {
@Base("fastbin") {
@Header() {
<a href="/" class="inline-block text-center align-middle">
@Button() {
<span class="material-symbols-outlined">note_add</span>
}
</a>
}
<div class="flex flex-1 w-full text-white text-xl">
<div class="h-full px-2.5 select-none">></div>
<div class="flex w-full h-full bg-transparent resize-none text-white justify-center items-center text-8xl"><div>500: Internal Server Error</div></div>
</div>
}
}

View file

@ -0,0 +1,36 @@
package views
templ Base(title string) {
<html>
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined" />
<script src="assets/js/htmx.min.js"></script>
<link rel="stylesheet" href="assets/css/output.css"/>
<meta name="description" content="fastbin: sharing code made faster" />
<link rel="icon" href="/assets/favicon.ico" />
<title>{ title }</title>
</head>
<body>
<main class="w-dvw h-dvh bg-primary flex flex-col items-start justify-center">
{ children... }
</main>
</body>
</html>
}
templ Header() {
<div class="w-full flex justify-between items-center text-white border-b-2 border-b-border">
<div class="px-[20px] py-[10px] text-4xl italic select-none pointer-events-none">fastbin</div>
<div class="flex self-stretch items-stretch">
{ children... }
</div>
</div>
}
templ Button() {
<div class="w-[70px] h-[70px] flex justify-center align-middle items-center text-center cursor-pointer select-none hover:bg-border/50">
{ children... }
</div>
}

View file

@ -0,0 +1,17 @@
package views
templ Read(text string) {
@Base("fastbin") {
@Header() {
<a href="/" class="inline-block text-center align-middle">
@Button() {
<span class="material-symbols-outlined">note_add</span>
}
</a>
}
<div class="flex flex-1 w-full text-white text-xl">
<div class="h-full px-2.5 select-none">></div>
<textarea required readonly name="text" id="input" autofocus wrap="off" spellcheck="false" class="flex-1 w-full h-full bg-transparent resize-none outline-none border-none text-white text-xl">{ text } </textarea>
</div>
}
}

View file

@ -0,0 +1,41 @@
package views
var onceHandle = templ.NewOnceHandle()
templ Write() {
@Base("fastbin") {
<form id="input_form" hx-post="/" class="w-dvw h-dvh bg-primary flex flex-col items-start justify-center">
@Header() {
<a href="/" class="inline-block text-center align-middle">
@Button() {
<span class="material-symbols-outlined">note_add</span>
}
</a>
<a>
<button type="submit">
@Button() {
<span class="material-symbols-outlined">save</span>
}
</button>
</a>
}
<script type="text/javascript">
function keyDownHandler(e) {
if (e.key === "Tab") {
e.preventDefault()
e.currentTarget.setRangeText(
'\t',
e.currentTarget.selectionStart,
e.currentTarget.selectionStart,
'end'
)
}
}
</script>
<div class="flex flex-1 w-full text-white text-xl">
<div class="h-full px-2.5 select-none">></div>
<textarea required name="text" id="input" onkeydown="keyDownHandler(event)" autofocus wrap="off" spellcheck="false" class="flex-1 w-full h-full bg-transparent resize-none outline-none border-none text-white text-xl"></textarea>
</div>
</form>
}
}

View file

@ -1,10 +0,0 @@
import faunadb from "faunadb";
const client = new faunadb.Client({
secret: process.env.FAUNA_ADMIN_KEY || "",
domain: "db.fauna.com",
port: 443,
scheme: "https",
});
export default client;

5
next-env.d.ts vendored
View file

@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View file

@ -1,6 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}
module.exports = nextConfig

View file

@ -1,30 +0,0 @@
{
"name": "fastbin",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2",
"@mui/material": "^5.4.3",
"faunadb": "^4.5.2",
"highlight.js": "^11.4.0",
"next": "12.1.0",
"react": "17.0.2",
"react-dom": "17.0.2"
},
"devDependencies": {
"@types/node": "17.0.18",
"@types/react": "17.0.39",
"eslint": "8.9.0",
"eslint-config-next": "12.1.0",
"typescript": "4.5.5"
}
}

View file

@ -1,104 +0,0 @@
import { useEffect, createRef } from 'react'
import Head from 'next/head'
import { useRouter } from 'next/router'
import hljs from 'highlight.js'
import styles from '../styles/Viewer.module.css'
import header_styles from '../styles/Header.module.css'
import 'highlight.js/styles/atom-one-dark.css';
import NoteAdd from '@material-ui/icons/NoteAdd'
import { GetServerSidePropsContext } from 'next'
import { getData } from './api/get/[id]'
const Viewer = ({ code }: { code: string }) => {
const codeRef = createRef<HTMLTextAreaElement>();
const router = useRouter()
const lines = code.split('\n');
const html = hljs.highlightAuto(code);
useEffect(() => {
if (codeRef.current)
codeRef.current.innerHTML = html.value;
}, [html, codeRef])
useEffect(() => {
const listener = (event: KeyboardEvent) => {
if (event.code === "KeyN" && event.shiftKey === true) {
event.preventDefault()
router.push('/')
}
}
document.addEventListener('keydown', listener)
return () => {
document.removeEventListener('keydown', listener)
}
}, [router])
return (
<div className={styles.container}>
<Head>
<title>fastbin</title>
<meta name="description" content="fastbin: sharing code made faster" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className={header_styles["header"]}>
<div className={header_styles["logo"]}> fastbin </div>
<div className={header_styles["buttons-container"]}>
<div
className={header_styles["buttons"]}
onClick={() => router.push('/')}
><NoteAdd /></div>
</div>
</div>
<div className={styles['viewer']}>
<code className={styles["line-numbers"]}>
{
lines.map((_line, index) => <pre key={index}> {index + 1} </pre>)
}
</code>
<pre className={styles["code"]}>
<code ref={codeRef}>
</code>
</pre>
</div>
</div>
)
}
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const id = context.params?.id
if (!id)
return {
redirect: {
destination: '/',
permanent: false,
}
}
try {
let data = await getData(id.toString())
return {
props: {
code: data.data?.code,
}
}
}
catch {
return {
redirect: {
destination: '/',
permanent: false,
}
}
}
}
export default Viewer

View file

@ -1,8 +0,0 @@
import '../styles/globals.css'
import type { AppProps } from 'next/app'
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
export default MyApp

View file

@ -1,15 +0,0 @@
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html>
<Head>
{/*<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" key="material-icons"></link>**/}
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}

View file

@ -1,27 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { Collection, Get, Ref, Time } from "faunadb";
import client from "../../../lib/fauna-client";
type Data = {
code: string;
};
type FaunaQueryResponse = {
ref?: typeof Ref;
ts?: typeof Time;
data?: Data;
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
const id = req.query["id"];
getData(id.toString())
.then((ret) => res.status(200).json({ code: ret?.data?.code || "" }))
.catch(() => res.status(404).send({ code: "" }));
}
export async function getData(id: string) {
return client.query<FaunaQueryResponse>(Get(Ref(Collection("data"), id)));
}

View file

@ -1,27 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import faunadb from "faunadb";
import client from "../../lib/fauna-client";
const q = faunadb.query;
type Data = {
id: string;
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
if (req.method !== "POST") res.status(404).send({ id: "" });
const code = req.body.data;
client
.query<any>(q.Create(q.Collection("data"), { data: { code: code } }))
.then((response) => {
res.status(200).json({ id: response?.ref?.id });
})
.catch((_error) => {
res.status(404).send({ id: "" });
});
}

View file

@ -1,114 +0,0 @@
import { KeyboardEventHandler, useCallback, useEffect, useRef, useState } from 'react'
import type { NextPage } from 'next'
import Head from 'next/head'
import { useRouter } from 'next/router'
import styles from '../styles/Editor.module.css'
import header_styles from '../styles/Header.module.css'
import Save from '@material-ui/icons/Save'
import NoteAdd from '@material-ui/icons/NoteAdd'
import { Snackbar } from '@mui/material'
const Home: NextPage = () => {
const [uploading, setUploading] = useState(false);
const codeRef = useRef<HTMLTextAreaElement>(null)
const router = useRouter()
const save = useCallback(() => {
setUploading(true);
fetch('/api/new', {
'method': 'POST',
'headers': {
'Content-Type': 'application/json'
},
'body': JSON.stringify({ data: codeRef.current?.value })
}).then(res => res.json())
.then(({ id }) => router.push(`/${id}`))
.catch(() => router.push('/'))
}, [router])
useEffect(() => {
const listener = (event: KeyboardEvent) => {
if (event.code === "KeyS" && event.ctrlKey === true) {
event.preventDefault()
save()
}
if (event.code === "KeyN" && event.shiftKey === true) {
event.preventDefault()
router.push('/')
}
}
document.addEventListener('keydown', listener)
return () => {
document.removeEventListener('keydown', listener)
}
}, [save, router])
const keyDownHandler: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
if (e.key === "Tab") {
e.preventDefault()
e.currentTarget.setRangeText(
'\t',
e.currentTarget.selectionStart,
e.currentTarget.selectionStart,
'end'
)
}
}
useEffect(() => {
if (codeRef.current)
codeRef.current.focus();
}, [codeRef])
return (
<div className={styles.container}>
<Head>
<title>fastbin</title>
<meta name="description" content="fastbin: sharing code made faster" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className={header_styles["header"]}>
<div className={header_styles["logo"]}> fastbin </div>
<div className={header_styles["buttons-container"]}>
<div
className={header_styles["buttons"]}
onClick={() => router.push('/')}
>
<NoteAdd />
</div>
<div
className={header_styles["buttons"]}
onClick={save}
>
<Save />
</div>
</div>
</div>
<div className={styles.editor}>
<span className={styles["line-numbers"]}>
{">"}
</span>
<textarea
onKeyDown={keyDownHandler}
spellCheck={false}
wrap="off"
ref={codeRef}
placeholder={"Type Someting Here...\nCtrl + S to Save Document\nShift + N for New Document\n:)"}
className={styles["code-editor"]}>
</textarea>
</div>
<Snackbar open={uploading}><div className={styles.toast}>Uploading Document ...</div></Snackbar>
</div>
)
}
export default Home

View file

@ -1,4 +0,0 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,45 +0,0 @@
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
color: white;
align-items: flex-start;
justify-content: center;
}
.editor {
display: flex;
color: white;
width: 100%;
height: 100%;
flex: 1;
padding-top: 10px;
}
.editor .line-numbers {
padding-left: 10px;
padding-right: 10px;
user-select: none;
}
.code-editor {
flex: 1;
padding: 3px;
overflow-x: auto;
resize: none;
outline: none;
background-color: transparent;
border: none;
color: #fff;
font-size: larger;
}
.toast {
padding: 0.5em;
padding-left: 0.75em;
padding-right: 0.75em;
background-color: rgb(0, 94, 255);
font-size:x-large;
border-radius: 15px;
}

View file

@ -1,33 +0,0 @@
.header {
display: flex;
color: white;
border-bottom-color: var(--border-color, white);
border-bottom-width: 2px;
border-bottom-style: solid;
width: 100%;
justify-content: space-between;
align-items: center;
}
.buttons-container {
display: flex;
}
.buttons {
padding: 15px;
cursor: pointer;
user-select: none;
}
.buttons:hover {
background-color: rgba(69, 69, 69, 0.5);
}
.logo {
padding-left: 20px;
padding-right: 20px;
font-size: xx-large;
font-style: italic;
pointer-events: none;
user-select: none;
}

View file

@ -1,33 +0,0 @@
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
color: white;
align-items: flex-start;
justify-content: center;
}
.viewer {
display: flex;
color: white;
width: 100%;
height: 100%;
flex: 1;
padding-top: 10px;
overflow: scroll;
}
.line-numbers {
padding-top: 3px;
padding-left: 10px;
padding-right: 10px;
user-select: none;
pointer-events: none;
}
.code {
flex: 1;
padding: 3px;
}

View file

@ -1,55 +0,0 @@
:root {
--bg-color: rgb(29, 29, 29);
--border-color: rgb(69, 69, 69);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body,
#__next {
width: 100%;
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
background-color: var(--bg-color);
}
a {
color: inherit;
text-decoration: none;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--bg-color) white;
}
*::-webkit-scrollbar {
width: 12px;
height: 12px;
cursor: default;
}
*::-webkit-scrollbar-track {
background: var(--bg-color);
}
*::-webkit-scrollbar-button {
display: none;
background: var(--bg-color);
}
*::-webkit-scrollbar-corner {
background: var(--bg-color);
}
*::-webkit-scrollbar-thumb {
border: 3px solid black;
border-radius: 6px;
background: rgb(255, 255, 255);
}

13
syntax.env Normal file
View file

@ -0,0 +1,13 @@
# API_INTERNAL_PORT=8080 # When using without docker
# KEYGEN_INTERNAL_PORT=8081 # When using without docker
API_URL=http://api-server
KEYGEN_PORT=8080
KEYGEN_HOST=localhost
DB_PORT=5432
DB_HOST=localhost
DB_DATABASE=fastbin
DB_USERNAME=username
DB_PASSWORD=password

15
tailwind.config.js Normal file
View file

@ -0,0 +1,15 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./internal/**/*.{go,js,templ,html}"
],
theme: {
extend: {
colors: {
"primary": "rgb(var(--primary-color) / <alpha-value>)",
"border": "rgb(var(--border-color) / <alpha-value>)"
}
},
},
plugins: [],
}

View file

@ -1,20 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

2481
yarn.lock

File diff suppressed because it is too large Load diff