Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ venv/
.env.test.local
.env.production.local
.vscode
projects.db

npm-debug.log*
yarn-debug.log*
Expand Down
145 changes: 145 additions & 0 deletions backend/API_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Storage API

A Flask-based REST API for providing storage capabilities (key-value pairs and file storage) using SQLite as the database.

## Features

- **Key-Value Storage**: Store and retrieve key-value pairs
- **File Storage**: Store, retrieve, list, rename, and delete files and directories
- **SQLite Database**: Persistent storage using SQLite
- **CORS Support**: Cross-origin resource sharing for frontend integration

## API Endpoints

### Key-Value Storage (Entries)

- `GET /entries/<entry_key>` - Fetch entry value by key (supports `?default=value` query param)
- `POST /entries/<entry_key>` - Save entry value

### File and Directory Operations

- `GET /storage/<path>` - List directory contents (if path ends with `/`) or fetch file content (if path doesn't end with `/`)
- `POST /storage/<path>` - Save file content (path must not end with `/`)
- `DELETE /storage/<path>` - Delete file or directory
- `POST /storage/rename` - Rename file or directory

## Running the Application

1. Install dependencies:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you include the step where you create venv?
See python_tools/README.md

```bash
pip install flask flask-restful flask-sqlalchemy
```

2. Run the application:
```bash
python main.py
```

3. The API will be available at `http://localhost:5001`

## Example Usage

### Key-Value Operations (Entries)

#### Save an Entry
```bash
curl -X POST http://localhost:5001/entries/user_settings \
-H "Content-Type: application/json" \
-d '{"value": "{\"theme\": \"dark\", \"language\": \"en\"}"}'
```

#### Fetch an Entry
```bash
curl http://localhost:5001/entries/user_settings
```

#### Fetch an Entry with Default Value
```bash
curl "http://localhost:5001/entries/missing_key?default=default_value"
```

### File and Directory Operations

#### Save a File
```bash
curl -X POST http://localhost:5001/storage/projects/robot1/main.py \
-H "Content-Type: application/json" \
-d '{"content": "# Robot main file\nprint(\"Hello Robot!\")"}'
```

#### Fetch a File
```bash
curl http://localhost:5001/storage/projects/robot1/main.py
```

#### List Files in Directory
```bash
curl http://localhost:5001/storage/projects/
```

#### Rename a File
```bash
curl -X POST http://localhost:5001/storage/rename \
-H "Content-Type: application/json" \
-d '{"old_path": "projects/robot1/main.py", "new_path": "projects/robot1/robot_main.py"}'
```

#### Rename a Directory
```bash
curl -X POST http://localhost:5001/storage/rename \
-H "Content-Type: application/json" \
-d '{"old_path": "projects/robot1/", "new_path": "projects/my_robot/"}'
```

#### Delete a File
```bash
curl -X DELETE http://localhost:5001/storage/projects/robot1/old_file.py
```

#### Delete a Directory
```bash
curl -X DELETE http://localhost:5001/storage/projects/old_project/
```

## Data Models

### StorageEntry (Key-Value Storage)
- `id`: Integer (Primary Key)
- `entry_key`: String (Unique, Required)
- `entry_value`: Text (Required)

### StorageFile (File Storage)
- `id`: Integer (Primary Key)
- `file_path`: String (Unique, Required)
- `file_content`: Text (Required)

## Database

The application creates a SQLite database file `storage.db` in the backend directory automatically when first run.

## CORS Support

The API includes CORS (Cross-Origin Resource Sharing) headers to allow frontend applications running on different ports to access the API. This is essential for development when the frontend runs on a different port than the backend.

## Frontend Integration

The server-side storage implementation in `src/storage/server_side_storage.ts` provides a TypeScript interface that connects to this Flask backend. It implements the `Storage` interface with methods for:

- Key-value storage (`saveEntry`, `fetchEntry`)
- File operations (`saveFile`, `fetchFileContentText`, `list`, `rename`, `delete`)

Example usage in TypeScript:
```typescript
import { ServerSideStorage } from './storage/server_side_storage';

const storage = new ServerSideStorage();

// Save and fetch key-value data
await storage.saveEntry('user_settings', JSON.stringify({theme: 'dark'}));
const settings = await storage.fetchEntry('user_settings', '{}');

// File operations
await storage.saveFile('projects/robot1/main.py', 'print("Hello Robot!")');
const content = await storage.fetchFileContentText('projects/robot1/main.py');
const files = await storage.list('projects/');
```
212 changes: 212 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
from flask import Flask, request, jsonify
from flask_restful import Resource, Api
from flask_sqlalchemy import SQLAlchemy
import json
import os

app = Flask(__name__)
api = Api(app)

# Add CORS headers
@app.after_request
def after_request(response):
response.headers.add('Access-Control-Allow-Origin', '*')
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
return response

@app.before_request
def handle_preflight():
if request.method == "OPTIONS":
response = jsonify({'message': 'OK'})
response.headers.add('Access-Control-Allow-Origin', '*')
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
return response

# Configure SQLite database
basedir = os.path.abspath(os.path.dirname(__file__))
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{os.path.join(basedir, "storage.db")}'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

# Storage models for key-value pairs and files
class StorageEntry(db.Model):
id = db.Column(db.Integer, primary_key=True)
entry_key = db.Column(db.String(255), nullable=False, unique=True)
entry_value = db.Column(db.Text, nullable=False)

def to_dict(self):
return {
'key': self.entry_key,
'value': self.entry_value
}

class StorageFile(db.Model):
id = db.Column(db.Integer, primary_key=True)
file_path = db.Column(db.String(500), nullable=False, unique=True)
file_content = db.Column(db.Text, nullable=False)

def to_dict(self):
return {
'path': self.file_path,
'content': self.file_content
}

# Storage Resources for key-value and file operations
class StorageEntryResource(Resource):
def get(self, entry_key):
"""Fetch entry value by key"""
entry = StorageEntry.query.filter_by(entry_key=entry_key).first()
if entry:
return {'value': entry.entry_value}
else:
# Return default value if provided in query params
default_value = request.args.get('default', '')
return {'value': default_value}

def post(self, entry_key):
"""Save entry value"""
data = request.get_json()
if not data or 'value' not in data:
return {'error': 'Entry value is required'}, 400

entry = StorageEntry.query.filter_by(entry_key=entry_key).first()
if entry:
entry.entry_value = data['value']
else:
entry = StorageEntry(entry_key=entry_key, entry_value=data['value'])
db.session.add(entry)

try:
db.session.commit()
return {'message': 'Entry saved successfully'}
except Exception as e:
db.session.rollback()
return {'error': 'Failed to save entry'}, 500

class StorageResource(Resource):
def get(self, path):
"""Get file content or list directory based on path"""
if path.endswith('/'):
# List directory
# Find all files that start with this path
files = StorageFile.query.filter(StorageFile.file_path.like(f'{path}%')).all()

# Extract immediate children (not nested)
children = set()
for file in files:
relative_path = file.file_path[len(path):]
if '/' in relative_path:
# It's in a subdirectory, add the directory name
dir_name = relative_path.split('/')[0] + '/'
children.add(dir_name)
else:
# It's a direct child file
children.add(relative_path)

return {'files': sorted(list(children))}
else:
# Get file content
file_obj = StorageFile.query.filter_by(file_path=path).first()
if file_obj:
return {'content': file_obj.file_content}
else:
return {'error': 'File not found'}, 404

def post(self, path):
"""Save file content (only for files, not directories)"""
if path.endswith('/'):
return {'error': 'Cannot save content to a directory path'}, 400

data = request.get_json()
if not data or 'content' not in data:
return {'error': 'File content is required'}, 400

file_obj = StorageFile.query.filter_by(file_path=path).first()
if file_obj:
file_obj.file_content = data['content']
else:
file_obj = StorageFile(file_path=path, file_content=data['content'])
db.session.add(file_obj)

try:
db.session.commit()
return {'message': 'File saved successfully'}
except Exception as e:
db.session.rollback()
return {'error': 'Failed to save file'}, 500

def delete(self, path):
"""Delete file or directory"""
if path.endswith('/'):
# Delete directory (all files starting with this path)
files = StorageFile.query.filter(StorageFile.file_path.like(f'{path}%')).all()
for file_obj in files:
db.session.delete(file_obj)
else:
# Delete specific file
file_obj = StorageFile.query.filter_by(file_path=path).first()
if not file_obj:
return {'error': 'File not found'}, 404
db.session.delete(file_obj)

try:
db.session.commit()
return {'message': 'Deleted successfully'}
except Exception as e:
db.session.rollback()
return {'error': 'Failed to delete'}, 500

class StorageFileRenameResource(Resource):
def post(self):
"""Rename file or directory"""
data = request.get_json()
if not data or 'old_path' not in data or 'new_path' not in data:
return {'error': 'Both old_path and new_path are required'}, 400

old_path = data['old_path']
new_path = data['new_path']

if old_path.endswith('/'):
# Rename directory (all files starting with old_path)
files = StorageFile.query.filter(StorageFile.file_path.like(f'{old_path}%')).all()
for file_obj in files:
new_file_path = file_obj.file_path.replace(old_path, new_path, 1)
file_obj.file_path = new_file_path
else:
# Rename specific file
file_obj = StorageFile.query.filter_by(file_path=old_path).first()
if not file_obj:
return {'error': 'File not found'}, 404
file_obj.file_path = new_path

try:
db.session.commit()
return {'message': 'Renamed successfully'}
except Exception as e:
db.session.rollback()
return {'error': 'Failed to rename'}, 500

# Register API routes
# Storage API routes (more specific routes first)
api.add_resource(StorageEntryResource, '/entries/<path:entry_key>')
api.add_resource(StorageFileRenameResource, '/storage/rename')
api.add_resource(StorageResource, '/storage/<path:path>')

@app.route('/')
def index():
return jsonify({
'message': 'Storage API',
'endpoints': {
'entries': '/entries/<entry_key>',
'storage': '/storage/<path>',
'storage_rename': '/storage/rename'
}
})

if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True, port=5001)
Binary file added backend/storage.db
Binary file not shown.
Loading