Compare commits

..
31 Commits
Author SHA1 Message Date
Théo LAGACHE 2a1acbfde8 chore(release): 1.1.9 2026-01-07 09:50:42 +01:00
Théo LAGACHEandGitHub 16ef8507b0 Merge pull request #6 from Portabase/readme
Readme
2026-01-07 09:48:56 +01:00
Théo LAGACHE df5d4e336c update: release 2026-01-07 09:46:30 +01:00
Théo LAGACHE ad2015329b chore(release): 1.1.9-rc6 2026-01-06 16:35:47 +01:00
Théo LAGACHE 4cc4bed785 chore(release): 1.1.9-rc5 2026-01-06 16:35:44 +01:00
Théo LAGACHE 6c506e6907 chore(release): 1.1.9-rc5 2026-01-06 16:25:12 +01:00
Théo LAGACHE 977f9344c7 chore(release): 1.1.9-rc4 2026-01-06 16:23:09 +01:00
Théo LAGACHE 2adb2f3b68 chore(release): 1.1.9-rc3 2026-01-06 16:21:06 +01:00
Théo LAGACHE 40a6e9e69f fix 2026-01-06 16:12:04 +01:00
Théo LAGACHE e1d1a3fb9a fix(ci): grant necessary permissions for docker publish workflow 2026-01-06 15:37:19 +01:00
Théo LAGACHE 75120c0a56 fix 2026-01-06 15:33:06 +01:00
Théo LAGACHE 33f1ef3bb3 ci: restore release candidate workflow and use reusable actions 2026-01-06 15:10:28 +01:00
Théo LAGACHE b6e742333c update: README.md 2026-01-06 13:26:52 +01:00
Théo LAGACHE 9f8c66fc20 update readme.md 2026-01-06 12:11:45 +01:00
charlesgauthereau 3b93257890 [Release] - 1.1.8 2026-01-05 19:41:53 +01:00
charlesgauthereau 1104e2a074 [FIX] (profile) - error in local upload (#4) 2026-01-05 19:39:59 +01:00
Théo LAGACHE 3e950652e8 update readme.md 2026-01-05 15:04:28 +01:00
charlesgauthereau 98e40466a0 [FIX] (agent) - order by createdAt 2026-01-04 21:24:06 +01:00
charlesgauthereau 324d80a735 [FIX] (backup) - change in extension file 2026-01-04 19:40:08 +01:00
charlesgauthereau 6770be63b6 [FIX] - adding fileSize in upload-backup.action.ts 2026-01-03 01:06:13 +01:00
charlesgauthereau 89aa33ebd8 Updating version. 2026-01-02 15:40:20 +01:00
charlesgauthereau 619c32f09d [FEATURE] - Adding fileSize in backup model in order to display it. 2026-01-02 15:32:36 +01:00
Charles GTEandGitHub c471423775 Update README.md 2026-01-02 15:06:24 +01:00
charlesgauthereau c589b9b8b8 Updating version. Ready for 1.1.7-rc.1 2026-01-02 14:24:37 +01:00
charlesgauthereau bae9ba0973 Increase proxyClientMaxBodySize to 10g in order to accept large databases. (#2) 2026-01-02 13:55:53 +01:00
charlesgauthereau 7bfc5c1d9b Increase proxyClientMaxBodySize to 10g in order to accept large databases. 2026-01-02 13:51:57 +01:00
charlesgauthereau 8bd3b0d731 Updating README.md 2026-01-01 19:10:20 +01:00
charlesgauthereau 06cd4de141 Moving to Github folder CONTRIBUTING.md 2026-01-01 19:08:40 +01:00
charlesgauthereau 33eb2df023 Adding CITATION.cff 2026-01-01 19:07:23 +01:00
charlesgauthereau 540ecd4b60 Updating README.md 2026-01-01 18:54:06 +01:00
charlesgauthereau 237418f009 Updating README.md 2026-01-01 18:49:18 +01:00
32 changed files with 16575 additions and 15254 deletions
+2 -2
View File
@@ -8,8 +8,8 @@
We take security seriously and aim to support the following versions of the project with security updates: We take security seriously and aim to support the following versions of the project with security updates:
| Version | Supported | | Version | Supported |
|-----------|--------------------| |---------|--------------------|
| 1.x | ✅ Fully Supported | | Latest | ✅ Fully Supported |
--- ---
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+63
View File
@@ -0,0 +1,63 @@
name: Docker Publish
on:
workflow_call:
inputs:
image_name:
required: false
type: string
default: 'solucetechnologies/portabase'
add_latest:
required: false
type: boolean
default: false
target:
required: false
type: string
default: 'prod'
dockerfile:
required: false
type: string
default: './docker/dockerfile/Dockerfile'
secrets:
DOCKER_USERNAME:
required: true
DOCKER_PASSWORD:
required: true
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Check out the repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Log in to Docker Hub
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set tags
id: set-tags
run: |
REF_NAME=${GITHUB_REF#refs/tags/}
TAGS="${{ inputs.image_name }}:$REF_NAME"
if [[ "${{ inputs.add_latest }}" == "true" ]]; then
TAGS="$TAGS,${{ inputs.image_name }}:latest"
fi
echo "tags=$TAGS" >> $GITHUB_OUTPUT
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ${{ inputs.dockerfile }}
push: true
tags: ${{ steps.set-tags.outputs.tags }}
target: ${{ inputs.target }}
+128
View File
@@ -0,0 +1,128 @@
name: GitHub Release
on:
workflow_call:
inputs:
prerelease:
required: false
type: boolean
default: false
make_latest:
required: false
type: boolean
default: false
discord_title:
required: true
type: string
discord_color:
required: true
type: number
discord_footer:
required: true
type: string
secrets:
DISCORD_WEBHOOK:
required: true
GH_TOKEN:
required: true
jobs:
create-release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Check out the repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build Changelog
id: build_changelog
uses: mikepenz/release-changelog-builder-action@v5
with:
mode: "COMMIT"
configurationJson: |
{
"template": "#{{CHANGELOG}}",
"categories": [
{
"title": "## Feature",
"labels": ["feat", "feature"]
},
{
"title": "## Fix",
"labels": ["fix", "bug"]
},
{
"title": "## Other",
"labels": []
}
],
"label_extractor": [
{
"pattern": "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\([\w\-\.]+\))?(!)?: ([\w ])+([\s\S]*)",
"on_property": "title",
"target": "$1"
}
]
}
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: false
body: ${{ steps.build_changelog.outputs.changelog }}
prerelease: ${{ inputs.prerelease }}
make_latest: ${{ inputs.make_latest }}
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
- name: Send Discord Notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
RELEASE_INFO=$(gh release view "${{ github.ref_name }}" -R ${{ github.repository }} --json name,url,body,author)
RELEASE_TITLE=$(echo "$RELEASE_INFO" | jq -r .name)
if [ -z "$RELEASE_TITLE" ] || [ "$RELEASE_TITLE" = "null" ]; then RELEASE_TITLE="${{ github.ref_name }}"; fi
RELEASE_URL=$(echo "$RELEASE_INFO" | jq -r .url)
RELEASE_BODY=$(echo "$RELEASE_INFO" | jq -r .body)
AUTHOR_NAME="Portabase"
AUTHOR_ICON="https://github.com/Portabase.png"
PAYLOAD=$(jq -n \
--arg title "$RELEASE_TITLE" \
--arg description "$RELEASE_BODY" \
--arg url "$RELEASE_URL" \
--arg author "$AUTHOR_NAME" \
--arg icon "$AUTHOR_ICON" \
--arg discord_title "${{ inputs.discord_title }}" \
--arg discord_footer "${{ inputs.discord_footer }}" \
--argjson discord_color ${{ inputs.discord_color }} \
'{
content: $discord_title,
embeds: [{
title: $title,
url: $url,
description: $description,
color: $discord_color,
author: {
name: $author,
icon_url: $icon
},
footer: {
text: $discord_footer
}
}]
}'
)
curl -H "Content-Type: application/json" \
-d "$PAYLOAD" \
"$DISCORD_WEBHOOK"
+32
View File
@@ -0,0 +1,32 @@
name: Publish Docker image for release candidate
on:
push:
tags:
- '*-rc*'
permissions:
contents: write
packages: write
jobs:
docker_publish:
uses: ./.github/workflows/docker.yml
with:
add_latest: false
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
github_release:
needs: docker_publish
uses: ./.github/workflows/github.yml
with:
prerelease: true
make_latest: false
discord_title: "||@release-dashboard|| New release candidate published"
discord_color: 16776960
discord_footer: "Portabase"
secrets:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+25 -42
View File
@@ -1,50 +1,33 @@
name: Publish Docker image for release name: Publish Docker image for release
on: on:
release: push:
types: [published] tags:
- '*.*.*'
- '!*-*'
permissions:
contents: write
packages: write
jobs: jobs:
push_to_registry: docker_publish:
name: Push Docker image to Docker Hub uses: ./.github/workflows/docker.yml
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
attestations: write
id-token: write
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Log in to Docker Hub
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
with: with:
username: ${{ secrets.DOCKER_USERNAME }} add_latest: true
password: ${{ secrets.DOCKER_PASSWORD }} secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker github_release:
id: meta needs: docker_publish
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 uses: ./.github/workflows/github.yml
with: with:
images: solucetechnologies/portabase prerelease: false
make_latest: true
- name: Set tags discord_title: "||@release-dashboard|| New release published"
id: set-tags discord_color: 5814783
run: | discord_footer: "Portabase"
echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV secrets:
TAGS="solucetechnologies/portabase:${GITHUB_REF#refs/tags/}" DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
if [ "${{ github.event.release.target_commitish }}" = "main" ]; then GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAGS="$TAGS,solucetechnologies/portabase:latest"
fi
echo "tags=$TAGS" >> $GITHUB_OUTPUT
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/dockerfile/Dockerfile
push: true
tags: ${{ steps.set-tags.outputs.tags }}
target: prod
Binary file not shown.
-2
View File
@@ -1,2 +0,0 @@
nodeLinker: node-modules
+30
View File
@@ -0,0 +1,30 @@
cff-version: 1.2.0
title: Portabase
message: "If you use this software, please cite it as below."
type: software
authors:
- family-names: Gauthereau
given-names: Charles
- family-names: Larcher
given-names: Killian
repository-code: https://github.com/Portabase/portabase
url: https://portabase.io
abstract: "Portabase is a free, open-source, self-hosted solution for database administration, providing backup and restore capabilities, scheduling, retention policies, notifications, and support for multiple storage backends. Its headless agent architecture enables connection to multiple database instances securely and efficiently."
keywords:
- docker
- kubernetes
- backups
- postgresql
- mysql
- mariadb
- devops
- database
- monitoring
- s3
- self-hosted
- system-administration
- web-ui
- agent
license: Apache-2.0
version: 1.1.9
date-released: "2026-01-07"
+27 -247
View File
@@ -1,29 +1,39 @@
<br /> <br />
<div align="center"> <div align="center">
<a href="https://portabase.io"> <a href="https://portabase.io">
<img src="/public/images/logo.png" alt="Logo" width="80" height="80"> <img src="/.github/assets/logo.png" alt="Logo" width="80" height="80">
</a> </a>
<h3 align="center">Portabase</h3> <h3 align="center">Portabase</h3>
<p>
Free, open-source, and self-hosted solution for automated backup and restoration of your database instances. <p align="center" style="margin-top: 20px; font-style: italic;">
<i>Portabase is a tool designed to simplify the backup and restoration of your database instances. It integrates seamlessly with <a href="https://github.com/Portabase/agent">Portabase agents</a> for managing operations securely and efficiently.</i>
</p> </p>
[![License: Apache](https://img.shields.io/badge/License-apache-yellow.svg)](LICENSE) [![License: Apache](https://img.shields.io/badge/License-apache-yellow.svg)](LICENSE)
[![Docker Pulls](https://img.shields.io/docker/pulls/solucetechnologies/portabase?color=brightgreen)](https://hub.docker.com/r/solucetechnologies/portabase) [![Docker Pulls](https://img.shields.io/docker/pulls/solucetechnologies/portabase?color=brightgreen)](https://hub.docker.com/r/solucetechnologies/portabase)
[![Platform](https://img.shields.io/badge/platform-linux%20%7C%20macos%20%7C%20windows-lightgrey)](https://github.com/RostislavDugin/postgresus) [![Platform](https://img.shields.io/badge/platform-linux%20%7C%20macos%20%7C%20windows-lightgrey)](https://github.com/Portabase/portabase)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-336791?logo=postgresql&logoColor=white)](https://www.postgresql.org/) [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-336791?logo=postgresql&logoColor=white)](https://www.postgresql.org/)
[![MySQL](https://img.shields.io/badge/MySQL-4479A1?logo=mysql&logoColor=white)](https://www.mysql.com/) [![MySQL](https://img.shields.io/badge/MySQL-4479A1?logo=mysql&logoColor=white)](https://www.mysql.com/)
[![MariaDB](https://img.shields.io/badge/MariaDB-003545?logo=mariadb&logoColor=white)](https://mariadb.org/) [![MariaDB](https://img.shields.io/badge/MariaDB-003545?logo=mariadb&logoColor=white)](https://mariadb.org/)
[![Self Hosted](https://img.shields.io/badge/self--hosted-yes-brightgreen)](https://github.com/RostislavDugin/postgresus) [![Self Hosted](https://img.shields.io/badge/self--hosted-yes-brightgreen)](https://github.com/Portabase/portabase)
[![Open Source](https://img.shields.io/badge/open%20source-❤️-red)](https://github.com/Portabase/portabase)
[![NextJS][NextJS]][NextJS-url]
[![BetterAuth][BetterAuth]][BetterAuth-url]
[![Drizzle][Drizzle]][Drizzle-url]
[![ShadcnUI][ShadcnUI]][ShadcnUI-url]
[![Docker][Docker]][Docker-url]
<p> <p>
<strong> <strong>
<a href="https://portabase.io">Documentation</a> • <a href="https://portabase.io">Website</a> •
<a href="https://portabase.io/docs">Documentation</a> •
<a href="https://www.youtube.com/watch?v=hvLbX5LN1UE">Demo</a> • <a href="https://www.youtube.com/watch?v=hvLbX5LN1UE">Demo</a> •
<a href="#installation">Installation</a> • <a href="https://portabase.io/docs/dashboard/setup">Installation</a> •
<a href="#contributing">Contributing</a> •
<a href="https://github.com/Portabase/portabase/issues/new?labels=bug&template=bug-report---.md">Report Bug</a> • <a href="https://github.com/Portabase/portabase/issues/new?labels=bug&template=bug-report---.md">Report Bug</a> •
<a href="https://github.com/Portabase/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a> <a href="https://github.com/Portabase/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
</strong> </strong>
@@ -34,258 +44,28 @@
</div> </div>
## 📚 Table of Contents ## Installation
- [About The Project](#-about-the-project) You have 4 ways to install Portabase:
- [Getting Started](#-getting-started)
- [Usage](#-usage)
- [Roadmap](#-roadmap)
- [Contributing](#-contributing)
- [License](#-license)
- [Contact](#-contact)
- [Acknowledgments](#-acknowledgments)
--- - Automated CLI (recommended) - [details](https://portabase.io/docs/dashboard/setup#cli)
- Docker Compose setup - [details](https://portabase.io/docs/dashboard/setup#docker)
- Kubernetes with Helm (soon)
- Development setup - [details](https://portabase.io/docs/dashboard/setup#development)
## ✨ About The Project **Ensure Docker is installed on your machine before getting started.**
**Portabase** is a server dashboard tool designed to simplify the backup and restoration of your database instances. It
integrates seamlessly with Portabase agents for managing operations securely and efficiently.
GitHub Repository: [Portabase](https://github.com/Portabase/portabase)
### 🔧 Built With ## Contributors
- [![NextJS][NextJS]][NextJS-url] (v16 with App Router)
- [![Drizzle][Drizzle]][Drizzle-url]
- [![ShadcnUI][ShadcnUI]][ShadcnUI-url]
- [![BetterAuth][BetterAuth]][BetterAuth-url]
- [![Docker][Docker]][Docker-url]
---
## 🚀 Getting Started
### Installation
Ensure Docker is installed on your machine before getting started.
### Option 1: Docker Compose Setup
Create a `docker-compose.yml` file with the following configuration:
```yaml
name: portabase
services:
portabase:
image: solucetechnologies/portabase:latest
env_file:
- .env
ports:
- '8887:80'
environment:
- TZ="Europe/Paris"
volumes:
- portabase-private:/app/private
depends_on:
db:
condition: service_healthy
container_name: portabase-app
db:
image: postgres:17-alpine
ports:
- "5433:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=<your_database>
- POSTGRES_USER=<database_user>
- POSTGRES_PASSWORD=<database_password>
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U <database_user> -d <your_database>" ]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres-data:
portabase-private:
```
Then run:
```bash
docker compose up -d
```
If you use reverse proxy like
Traefik : [Check this link](https://portabase.io/docs/dashboard/advanced/reverse-proxy)
### Option 2: Locally (Development)
1. Clone the repository:
```bash
git clone https://github.com/Portabase/portabase
cd portabase
```
2. Start the development environment:
```bash
docker compose up
```
---
## 🛠️ Usage
Portabase provides a web dashboard to manage your database instances and backups.
It currently supports:
- **PostgreSQL**
- **MySQL**
### Process
1. **Access the dashboard** Open `http://localhost:8887` in your browser.
2. **Sign up** Register the first user, who will automatically have the **Admin** role in the default workspace.
3. **Add your first agent** Follow [this guide](https://github.com/Portabase/agent-portabase) for setup
instructions.
4. **Create organizations and projects** Link your databases to projects to enable backups and restores.
5. **Configure backup policies** Define schedules (hourly, daily, weekly, or monthly) and retention rules.
6. **Choose a storage provider** Select where backups will be stored (local, S3, etc.).
7. **Save and start** Portabase validates your configuration and starts automated backups based on your defined
policies.
---
## 🗺️ Roadmap
- [ ] Add changelog
- [ ] Enhance documentation
- [ ] Implement testing procedures
- [ ] Include release file
- [ ] Improve security
- [ ] Enhance UX/UI
- [ ] Improve workspace management system
- [x] Migrate to Drizzle ORM
- [ ] Add multi-language support
- [ ] Extend multi-database support:
- [x] PostgreSQL
- [ ] MongoDB
- [x] MySQL
- [x] MariaDB
Check out [open issues](https://github.com/Portabase/portabase/issues) for more.
---
## 🤝 Contributing
Contributions are welcome and appreciated! Here's how to get started:
1. Fork the repository
2. Create a new branch:
```bash
git checkout -b feature/YourFeature
```
3. Commit your changes:
```bash
git commit -m "Add YourFeature"
```
4. Push to the branch:
```bash
git push origin feature/YourFeature
```
5. Open a pull request
Give the project a ⭐ if you like it!
### Top Contributors
[![Contributors](https://contrib.rocks/image?repo=Portabase/portabase)](https://github.com/Portabase/portabase/graphs/contributors) [![Contributors](https://contrib.rocks/image?repo=Portabase/portabase)](https://github.com/Portabase/portabase/graphs/contributors)
---
## Developer Notes ## License
### Environment Variables
```yml
# Environment
NODE_ENV=production
# Database
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
# Project Info
PROJECT_NAME="Portabase"
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
PROJECT_URL=http://app.portabase.io
PROJECT_SECRET=
# SMTP (Email)
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=
# Google OAuth
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
AUTH_GOOGLE_METHOD=
# S3/MinIO Configuration
S3_ENDPOINT=http://app.s3.portabase.io
S3_ACCESS_KEY=
S3_SECRET_KEY=
S3_BUCKET_NAME=portabase
S3_PORT=9000
S3_USE_SSL=true
# Storage Backend: 'local' or 's3'
STORAGE_TYPE=local
# Retention
RETENTION_CRON="* * * * *"
```
To get more information about env variables, check
that [link](https://portabase.io/docs/portabase/advanced-topics/environment)
### Semantic Versioning
Use the following format for Docker image versioning:
```bash
major.minor.patch-rc.release
# Example: 1.0.0-rc.1
major.minor.patch-rc.release-tag
# Example: 1.0.0-rc.1-dev
```
---
## 📄 License
Distributed under the Apache License. See `LICENSE.txt` for more details. Distributed under the Apache License. See `LICENSE.txt` for more details.
---
## 📬 Contact
- Killian Larcher - killian.larcher@soluce-technologies.com
- Charles Gauthereau - charles.gauthereau@soluce-technologies.com
- Project Link: [Portabase GitHub](https://github.com/Portabase/portabase)
---
## 🙏 Acknowledgments
Thanks to all contributors and the open-source community!
[Docker]: https://img.shields.io/badge/Docker-2496ED?logo=docker&logoColor=fff&style=for-the-badge [Docker]: https://img.shields.io/badge/Docker-2496ED?logo=docker&logoColor=fff&style=for-the-badge
@@ -7,7 +7,7 @@ import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/
import {notFound} from "next/navigation"; import {notFound} from "next/navigation";
import {db} from "@/db"; import {db} from "@/db";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {eq, not} from "drizzle-orm"; import {desc, eq, not} from "drizzle-orm";
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder"; import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
import {Metadata} from "next"; import {Metadata} from "next";
@@ -21,7 +21,8 @@ export default async function RoutePage(props: PageParams<{}>) {
where: not(eq(drizzleDb.schemas.agent.isArchived, true)), where: not(eq(drizzleDb.schemas.agent.isArchived, true)),
with: { with: {
databases: true databases: true
} },
orderBy: (fields) => desc(fields.createdAt),
}); });
console.log(agents); console.log(agents);
+5 -3
View File
@@ -106,7 +106,7 @@ export async function POST(
if (status === "success") { if (status === "success") {
const file = formData.get("file") as File | null; const file = formData.get("file") as File | null;
const extension = formData.get("extension") as string | null;
if (!aesKeyHex || !ivHex) { if (!aesKeyHex || !ivHex) {
return NextResponse.json({error: "Missing fields"}, {status: 400}); return NextResponse.json({error: "Missing fields"}, {status: 400});
} }
@@ -118,12 +118,13 @@ export async function POST(
{status: 400} {status: 400}
); );
} }
const fileExtension = getFileExtension(database.dbms) const fileSizeBytes = file.size;
// const fileExtension = '.' + (file.name.split('.').pop()?.toLowerCase() || '');
const fileExtension = extension ? extension : getFileExtension(database.dbms)
const decryptedFile = await decryptedDump(file, aesKeyHex, ivHex, fileExtension); const decryptedFile = await decryptedDump(file, aesKeyHex, ivHex, fileExtension);
const uuid = uuidv4(); const uuid = uuidv4();
const fileName = `${uuid}${fileExtension}`; const fileName = `${uuid}${fileExtension}`;
const buffer = Buffer.from(await decryptedFile.arrayBuffer()); const buffer = Buffer.from(await decryptedFile.arrayBuffer());
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1); const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
if (!settings) { if (!settings) {
throw new Error("System settings not found."); throw new Error("System settings not found.");
@@ -149,6 +150,7 @@ export async function POST(
.update(drizzleDb.schemas.backup) .update(drizzleDb.schemas.backup)
.set(withUpdatedAt({ .set(withUpdatedAt({
file: fileName, file: fileName,
fileSize: fileSizeBytes,
status: 'success', status: 'success',
})) }))
.where(eq(drizzleDb.schemas.backup.id, backup.id)); .where(eq(drizzleDb.schemas.backup.id, backup.id));
+26 -34
View File
@@ -1,45 +1,37 @@
FROM node:22-alpine AS base FROM node:22-alpine AS base
ENV YARN_VERSION=4.9.1
RUN apk add --update --no-cache \ RUN apk add --update --no-cache \
tzdata \
build-base \
libc6-compat \ libc6-compat \
openssl \ openssl \
make \ tzdata
FROM base AS build-env
RUN apk add --update --no-cache \
build-base \
g++ \ g++ \
jpeg-dev \ make \
cairo-dev \
giflib-dev \
pango-dev \
libtool \ libtool \
autoconf \ autoconf \
automake \ automake
libpng
RUN corepack enable && corepack prepare yarn@${YARN_VERSION} RUN corepack enable && corepack prepare pnpm@latest --activate
RUN yarn set version ${YARN_VERSION}
FROM base AS deps
FROM build-env AS deps
WORKDIR /app WORKDIR /app
COPY .yarn ./.yarn COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY .yarnrc.yml ./
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ RUN pnpm i --frozen-lockfile
RUN \
if [ -f yarn.lock ]; then yarn; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
FROM base AS dev FROM build-env AS dev
WORKDIR /app WORKDIR /app
@@ -52,8 +44,11 @@ RUN chmod +x /app/docker/entrypoints/app-dev-entrypoint.sh
ENTRYPOINT ["sh","/app/docker/entrypoints/app-dev-entrypoint.sh"] ENTRYPOINT ["sh","/app/docker/entrypoints/app-dev-entrypoint.sh"]
# Rebuild the source code only when needed
FROM base AS builder
FROM build-env AS builder
WORKDIR /app WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
@@ -61,12 +56,11 @@ COPY . .
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
RUN \ RUN pnpm run build
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
FROM base AS prod FROM base AS prod
@@ -109,5 +103,3 @@ ENV HOSTNAME="0.0.0.0"
USER nextjs USER nextjs
ENTRYPOINT ["sh","/app/app-prod-entrypoint.sh"] ENTRYPOINT ["sh","/app/app-prod-entrypoint.sh"]
+3 -3
View File
@@ -3,10 +3,10 @@
set -euo pipefail set -euo pipefail
echo "▶ Running Drizzle codegen..." echo "▶ Running Drizzle codegen..."
npx drizzle-kit generate pnpm drizzle-kit generate
echo "▶ Applying migrations..." echo "▶ Applying migrations..."
npx drizzle-kit migrate pnpm drizzle-kit migrate
echo "▶ Starting Next.js dev server..." echo "▶ Starting Next.js dev server..."
exec npm run dev exec pnpm dev
+3 -1
View File
@@ -43,8 +43,10 @@ const nextConfig: NextConfig = {
}, },
experimental: { experimental: {
serverActions: { serverActions: {
bodySizeLimit: "50mb", bodySizeLimit: "10gb",
}, },
proxyClientMaxBodySize: '10gb',
}, },
async headers() { async headers() {
return [ return [
+4 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "portabase", "name": "portabase",
"version": "1.1.6", "version": "1.1.9",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack -p 8887", "dev": "next dev --turbopack -p 8887",
@@ -97,6 +97,7 @@
"devDependencies": { "devDependencies": {
"@iconify/react": "^6.0.0", "@iconify/react": "^6.0.0",
"@react-email/preview-server": "4.3.2", "@react-email/preview-server": "4.3.2",
"@react-email/render": "^2.0.1",
"@tailwindcss/postcss": "^4.1.7", "@tailwindcss/postcss": "^4.1.7",
"@types/eslint-plugin-tailwindcss": "^3.17.0", "@types/eslint-plugin-tailwindcss": "^3.17.0",
"@types/node": "^22.15.18", "@types/node": "^22.15.18",
@@ -111,6 +112,7 @@
"eslint": "^9.39.0", "eslint": "^9.39.0",
"eslint-config-next": "^16.0.1", "eslint-config-next": "^16.0.1",
"eslint-plugin-tailwindcss": "^3.18.0", "eslint-plugin-tailwindcss": "^3.18.0",
"framer-motion": "^12.24.7",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"tailwindcss": "^4.1.7", "tailwindcss": "^4.1.7",
"tsx": "^4.19.4", "tsx": "^4.19.4",
@@ -118,5 +120,5 @@
"typescript": "^5.8.3", "typescript": "^5.8.3",
"zenstack": "2.14.2" "zenstack": "2.14.2"
}, },
"packageManager": "yarn@4.9.1" "packageManager": "pnpm@10.27.0"
} }
+14158
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
onlyBuiltDependencies:
- zenstack
Executable
+95
View File
@@ -0,0 +1,95 @@
#!/bin/bash
set -e
if [ -z "$1" ]; then
echo "Usage: ./release <version>"
echo "Example: ./release v1.0.0"
exit 1
fi
VERSION=$1
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "$CURRENT_BRANCH" = "main" ]; then
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: On 'main' branch, only release tags (X.Y.Z) are allowed."
exit 1
fi
else
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
echo "Error: On branch '$CURRENT_BRANCH', only RC tags matching X.Y.Z-rc.W are allowed (e.g., 1.0.0-rc.1)."
exit 1
fi
fi
CLEAN_VERSION=${VERSION#v}
CURRENT_DATE=$(date +%Y-%m-%d)
echo "Preparing release $VERSION..."
# package.json
if [ -f package.json ]; then
echo "Updating package.json..."
if sed --version >/dev/null 2>&1; then
sed -i "s/\"version\": \".*\"/\"version\": \"$CLEAN_VERSION\"/" package.json
else
sed -i '' "s/\"version\": \".*\"/\"version\": \"$CLEAN_VERSION\"/" package.json
fi
fi
# pyproject.toml
if [ -f pyproject.toml ]; then
echo "Updating pyproject.toml..."
if sed --version >/dev/null 2>&1; then
sed -i "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" pyproject.toml
else
sed -i '' "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" pyproject.toml
fi
fi
# Cargo.toml
if [ -f Cargo.toml ]; then
echo "Updating Cargo.toml..."
if sed --version >/dev/null 2>&1; then
sed -i "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" Cargo.toml
else
sed -i '' "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" Cargo.toml
fi
fi
# CITATION.cff
if [ -f CITATION.cff ]; then
echo "Updating CITATION.cff..."
if sed --version >/dev/null 2>&1; then
sed -i "s/^version: .*/version: $CLEAN_VERSION/" CITATION.cff
sed -i "s/^date-released: .*/date-released: \"$CURRENT_DATE\"/" CITATION.cff
else
sed -i '' "s/^version: .*/version: $CLEAN_VERSION/" CITATION.cff
sed -i '' "s/^date-released: .*/date-released: \"$CURRENT_DATE\"/" CITATION.cff
fi
fi
git add .
if ! git diff-index --quiet HEAD --; then
echo "Committing changes..."
git commit -m "chore(release): $VERSION"
else
echo "No changes to commit. Proceeding to tag..."
fi
if git rev-parse "$VERSION" >/dev/null 2>&1; then
echo "Tag $VERSION already exists. Aborting."
exit 1
fi
echo "Creating tag $VERSION..."
git tag -a "$VERSION" -m "Release $VERSION"
echo "Pushing changes and tags to remote..."
git push
git push origin "$VERSION"
echo "Successfully released $VERSION!"
@@ -42,6 +42,7 @@ export const uploadBackupAction = userAction
const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await file.arrayBuffer();
const fileSize = file.size;
const uuid = uuidv4(); const uuid = uuidv4();
const fileName = `imported_${uuid}${fileExtension}`; const fileName = `imported_${uuid}${fileExtension}`;
const buffer = Buffer.from(arrayBuffer); const buffer = Buffer.from(arrayBuffer);
@@ -85,6 +86,7 @@ export const uploadBackupAction = userAction
status: 'success', status: 'success',
databaseId: database.id, databaseId: database.id,
file: fileName, file: fileName,
fileSize: fileSize,
}) })
.returning(); .returning();
@@ -1,12 +1,12 @@
"use client"; "use client";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar";
import { UploadIcon } from "lucide-react"; import {UploadIcon} from "lucide-react";
import { toast } from "sonner"; import {toast} from "sonner";
import { uploadImageAction } from "@/features/upload/public/upload.action"; import {uploadImageAction} from "@/features/upload/public/upload.action";
import { useMutation } from "@tanstack/react-query"; import {useMutation} from "@tanstack/react-query";
import { updateImageUserAction } from "@/components/wrappers/dashboard/profile/actions/avatar.action"; import {updateImageUserAction} from "@/components/wrappers/dashboard/profile/actions/avatar.action";
import { useRouter } from "next/navigation"; import {useRouter} from "next/navigation";
import { User } from "@/db/schema/02_user"; import {User} from "@/db/schema/02_user";
import React, {ChangeEvent} from "react"; import React, {ChangeEvent} from "react";
export type AvatarWithUploadProps = { export type AvatarWithUploadProps = {
@@ -55,12 +55,11 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
}; };
return ( return (
<div className="relative "> <div className="relative ">
<Avatar className="w-24 h-24 lg:w-32 lg:h-32 border-4 border-muted/20"> <Avatar className="w-24 h-24 lg:w-32 lg:h-32 border-4 border-muted/20">
<AvatarImage src={user.image || undefined}/> <AvatarImage className="object-cover" src={user.image || undefined}/>
<AvatarFallback className="text-3xl">{user.name.charAt(0)}</AvatarFallback> <AvatarFallback className="text-3xl">{user.name.charAt(0)}</AvatarFallback>
</Avatar> </Avatar>
@@ -75,7 +74,7 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
}} }}
className="cursor-pointer absolute inset-0 flex justify-center items-center opacity-0 transition-opacity hover:opacity-100 hover:bg-gray-500 hover:bg-opacity-50 rounded-full w-24 h-24 lg:w-32 lg:h-32" className="cursor-pointer absolute inset-0 flex justify-center items-center opacity-0 transition-opacity hover:opacity-100 hover:bg-gray-500 hover:bg-opacity-50 rounded-full w-24 h-24 lg:w-32 lg:h-32"
> >
<UploadIcon className="w-12 h-12 lg:w-16 lg:h-16 text-primary" /> <UploadIcon className="w-12 h-12 lg:w-16 lg:h-16 text-primary"/>
</div> </div>
</div> </div>
); );
@@ -0,0 +1 @@
ALTER TABLE "backups" ADD COLUMN "file_size" integer;
File diff suppressed because it is too large Load Diff
+7
View File
@@ -113,6 +113,13 @@
"when": 1766426190521, "when": 1766426190521,
"tag": "0015_absurd_next_avengers", "tag": "0015_absurd_next_avengers",
"breakpoints": true "breakpoints": true
},
{
"idx": 16,
"version": "7",
"when": 1767363779637,
"tag": "0016_broken_morgan_stark",
"breakpoints": true
} }
] ]
} }
+1
View File
@@ -38,6 +38,7 @@ export const backup = pgTable(
id: uuid("id").primaryKey().defaultRandom(), id: uuid("id").primaryKey().defaultRandom(),
status: statusEnum("status").default("waiting").notNull(), status: statusEnum("status").default("waiting").notNull(),
file: text("file"), file: text("file"),
fileSize: integer("file_size"),
databaseId: uuid("database_id") databaseId: uuid("database_id")
.notNull() .notNull()
.references(() => database.id, {onDelete: "cascade"}), .references(() => database.id, {onDelete: "cascade"}),
+9 -1
View File
@@ -31,7 +31,7 @@ import {cn} from "@/lib/utils";
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip"; import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
import {MemberWithUser} from "@/db/schema/03_organization"; import {MemberWithUser} from "@/db/schema/03_organization";
import {formatLocalizedDate} from "@/utils/date-formatting"; import {formatLocalizedDate} from "@/utils/date-formatting";
import {isImportedFilename} from "@/utils/text"; import {formatBytes, isImportedFilename} from "@/utils/text";
export function backupColumns( export function backupColumns(
@@ -73,6 +73,14 @@ export function backupColumns(
return isImported ? `${reference} (imported)` : `${reference}` return isImported ? `${reference} (imported)` : `${reference}`
}, },
}, },
{
accessorKey: "fileSize",
header: "Size",
cell: ({row}) => {
console.log(row.original.fileSize)
return formatBytes(row.getValue("fileSize"))
},
},
{ {
accessorKey: "createdAt", accessorKey: "createdAt",
header: "Created At", header: "Created At",
+4 -4
View File
@@ -15,7 +15,7 @@ import {Setting} from "@/db/schema/01_setting";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
const privateS3ImageDir = "images/"; const imageDir = "images/";
export const uploadImageAction = userAction export const uploadImageAction = userAction
@@ -39,10 +39,10 @@ export const uploadImageAction = userAction
let result: void | UploadedObjectInfo; let result: void | UploadedObjectInfo;
if (settings.storage === "local") { if (settings.storage === "local") {
fileName = `${uuid}.${fileFormat}`; fileName = `${imageDir}${uuid}.${fileFormat}`;
result = await uploadLocal(fileName, buffer); result = await uploadLocal(fileName, buffer);
} else if (settings.storage === "s3") { } else if (settings.storage === "s3") {
fileName = `${privateS3ImageDir}${uuid}.${fileFormat}`; fileName = `${imageDir}${uuid}.${fileFormat}`;
result = await uploadS3Compatible(env.S3_BUCKET_NAME ?? "", fileName, buffer); result = await uploadS3Compatible(env.S3_BUCKET_NAME ?? "", fileName, buffer);
} else { } else {
throw new Error(`Unsupported storage type: ${settings.storage}`); throw new Error(`Unsupported storage type: ${settings.storage}`);
@@ -54,7 +54,7 @@ export const uploadImageAction = userAction
async function uploadLocal(fileName: string, buffer: any) { async function uploadLocal(fileName: string, buffer: any) {
const localDir = "private/uploads/images/"; const localDir = "private/uploads/";
try { try {
await mkdir(path.join(process.cwd(), localDir), {recursive: true}); await mkdir(path.join(process.cwd(), localDir), {recursive: true});
return await writeFile(path.join(process.cwd(), localDir + fileName), buffer); return await writeFile(path.join(process.cwd(), localDir + fileName), buffer);
+10
View File
@@ -16,3 +16,13 @@ export function isUUID(str: string) {
export function isImportedFilename(name: string): boolean { export function isImportedFilename(name: string): boolean {
return name.startsWith("imported_"); return name.startsWith("imported_");
} }
export function formatBytes(bytes: number | null, decimals = 2): string {
if (!bytes) return "N/A";
if (bytes === 0) return "0 Bytes";
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
}
-14896
View File
File diff suppressed because it is too large Load Diff