Compare commits

...

22 commits

Author SHA1 Message Date
Pakin
4709c3b2b7 feat: file history endpoint
- chore: cleaning some unused

Signed-off-by: Pakin <pakin.t@forth.co.th>
2026-07-06 10:51:59 +07:00
Pakin
2466472235 feat: add replay feature for recovery
- fix bug commit not update index

Signed-off-by: Pakin <pakin.t@forth.co.th>
2026-05-19 12:10:31 +07:00
Pakin
a69ef7b927 disable cache from redis
- reason from value not updated

Signed-off-by: Pakin <pakin.t@forth.co.th>
2026-05-18 09:56:23 +07:00
Pakin
b70a35135c update usage of commit
Signed-off-by: Pakin <pakin.t@forth.co.th>
2026-05-15 14:38:27 +07:00
Pakin
c8f820e238 add usages
Signed-off-by: Pakin <pakin.t@forth.co.th>
2026-05-15 14:27:18 +07:00
Pakin
caa0833ea2 fix: fail git clone by case empty folder
Signed-off-by: Pakin <pakin.t@forth.co.th>
2026-05-08 15:43:13 +07:00
Pakin
166b7079ca feat: add multi files support for commit
Signed-off-by: Pakin <pakin.t@forth.co.th>
2026-05-06 10:00:06 +07:00
Pakin
ae9d9fa66b feat: commit, push, pull
- test ok but must call pull first before do commit or push

Signed-off-by: Pakin <pakin.t@forth.co.th>
2026-05-05 16:55:09 +07:00
Pakin
bca1c911d3 add commit logging 2026-04-28 11:43:03 +07:00
Pakin
5bb2a6c192 change: change get from json cache file to multipart
Signed-off-by: Pakin <pakin.t@forth.co.th>
2026-04-27 15:47:11 +07:00
Pakin
d19dab7561 feat: pull handler
- test simple pull ok

Signed-off-by: Pakin <pakin.t@forth.co.th>
2026-04-10 10:11:53 +07:00
Pakin
bb3e55eecb feat: enable full clone
Signed-off-by: Pakin <pakin.t@forth.co.th>
2026-03-24 10:21:02 +07:00
Pakin
febf91d417 disable cold boot caching & optimizing
- caused by too much ram usage
- optimize memory usage on checkout code

Signed-off-by: Pakin <pakin.t@forth.co.th>
2026-02-25 15:11:29 +07:00
Pakin
6fe3357efe feat: add support for image file in checkout
Signed-off-by: Pakin <pakin.t@forth.co.th>
2026-02-17 15:17:04 +07:00
Pakin
a2da030a99 add tx, cold boot 2026-02-17 14:23:31 +07:00
Pakin
03263815e6 feat: fetch from cache if existed
Signed-off-by: Pakin <pakin.t@forth.co.th>
2026-02-02 14:12:16 +07:00
Pakin
3043f30012 feat: add commit, push handler
- add new feature commit changes and push to remote

Signed-off-by: Pakin <pakin.t@forth.co.th>
2026-01-22 17:20:03 +07:00
Pakin
2dd165b451 fix: cannot get file at root 2026-01-09 11:36:50 +07:00
Pakin
59d0dd7ab4 feature: enable get root files
- allow get files/dirs at root repo
2026-01-09 11:25:33 +07:00
Pakin
4e3b561f61 fix: missing token 2026-01-09 09:46:51 +07:00
Pakin
9f4fb6c274 update dockerfile 2026-01-08 14:40:58 +07:00
Pakin
90856717e4 update: repo & deps 2026-01-08 14:15:27 +07:00
11 changed files with 3291 additions and 1272 deletions

5
.dockerignore Normal file
View file

@ -0,0 +1,5 @@
/target
.tbcfg
*.txt
*.log

2
.gitignore vendored
View file

@ -1,5 +1,5 @@
/target
.tbcfg
.tbcfg*
*.txt
*.log

2653
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -4,19 +4,28 @@ version = "0.1.0"
edition = "2024"
[dependencies]
async-compression = { version = "0.4.39", features = ["tokio", "brotli"] }
axum = "0.8.7"
axum-macros = "0.5.0"
axum-extra = { version = "0.12.6", features = ["multipart"] }
bb8 = "0.9.1"
bb8-redis = "0.26.0"
brotli = "8.0.2"
bytes = "1.11.1"
env_logger = "0.11.8"
futures-util = "0.3.31"
git2 = { version = "0.20.3", features = ["https", "ssh"] }
image = "0.25.9"
json-patch = "4.1.0"
libgit2-sys = { version = "0.18.3", features = ["ssh"] }
libtbr = { git = "https://gitlab.forthrd.io/Pakin/libtbr.git", version = "0.1.1" }
libtbr = { git = "https://pakin-inspiron-15-3530.tail110d9.ts.net/pakin/libtbr.git", version = "0.1.1" }
log = "0.4.29"
prost = "0.14.1"
redis = { version = "1.0.2", features = ["tokio-comp"] }
reqwest = { version = "0.12.25", features = ["json"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = { version = "1.0.145", features = ["preserve_order"] }
tokio = { version = "1.48.0", features = ["full"] }
tonic = { version = "0.14.2", features = ["transport"] }
tonic-prost = "0.14.2"
[build-dependencies]
tonic-prost-build = "0.14.2"
tokio-util = { version = "0.7.18", features = ["io"] }
uuid = { version = "1.20.0", features = ["v4"] }
tokio-postgres = "0.7.17"
chrono = "0.4.45"

43
Dockerfile Normal file
View file

@ -0,0 +1,43 @@
FROM rustlang/rust:nightly-slim AS builder
WORKDIR /app
# Install build dependencies
RUN apt update && apt install -y \
musl-tools \
musl-dev \
build-essential \
libssl-dev \
curl \
pkg-config \
ca-certificates \
protobuf-compiler \
zlib1g
# Copy dependency files first for better caching
COPY Cargo.toml Cargo.lock ./
COPY ./src ./src
COPY build.rs ./
COPY tbm-proto ./tbm-proto
# Download config
RUN curl -X GET https://pakin-inspiron-15-3530.tail110d9.ts.net/pakin/tbm-git-repo-service/releases/download/config/.tbcfg -o .tbcfg
# Build the application
ENV RUSTFLAGS="-C target-feature=+crt-static"
RUN cargo build --release
# Builder
FROM gcr.io/distroless/cc
# Copy the binary
COPY --from=builder /app/target/release/tbm-git-repo-service /
COPY --from=builder /app/.tbcfg /
# Create data directory
EXPOSE 36583
# Environment defaults
CMD ["./tbm-git-repo-service"]

183
README.md Normal file
View file

@ -0,0 +1,183 @@
# tbm-git-service
Git repo as a service with operational endpoints
---
## Endpoints
### Checkout file
Get file content or list of file in directory from HEAD
```
GET .../checkout?path=<path_to_file>
```
Examples
Get list of files in directory
```bash
curl -X GET http://localhost:36593/checkout\?path\=inter
aus,common,dev,gbr,gbr_premium,hkg,ltu,mys,rou,sgp,tha,tha_premium,uae_dubai,usa,whatthecup
```
Get file
```bash
curl -X GET http://localhost:36593/checkout\?path\=inter/mys/xml/page_catalog_group_other.lxml
<Popup>
<Cache> "Enable" </Cache>
<Width> 1080 </Width>
<Height> 1920 </Height>
;<Background> "0xeae6e1" </Background>
<Volume> SoundVolume </Volume>
<EventOpen>
; On open
```
Error
```bash
curl -X GET http://localhost:36593/checkout\?path\=inter2
File not found
```
---
### Fetch
Fetching & update index of current repo
```
GET .../fetch
```
Expected result
```
{"result": "fetch success"}
```
Error
```
{"error": "..."}
```
---
### Commit
Commit changes of file i.e. editing, adding, etc.
```
POST .../commit
```
Body must be multipart
- Single file
```
path: string
file: text/binary is acceptable
signature_username: string
signature_email: stirng
message: string
```
- Multiple file
```
signature_username: string
signature_email: stirng
message: string
fileX1: text/binary is acceptable
pathX1: string
fileX2: text/binary is acceptable
pathX2: string
...
```
Expected result
```
{"result": "commit hash id"}
```
Error
```
{"error": "..."}
```
Example
```curl
curl --request POST \
--url http://localhost:36583/commit \
--header 'content-type: multipart/form-data' \
--form path=mys/version.dev \
--form 'signature_username=git api' \
--form signature_email=supra.m2.dev@forth.co.th \
--form 'message=test commit' \
--form file=@./mys.version.test
```
NOTE: file & path in multiple file must have the same name after
---
### Push
Push local commits to remote
```
GET .../push
```
Expected result
```
{"result": "push completed"}
```
Error
```
{"error": "..."}
```
---
### Pull
Pull commits from remote. This operation always does git reset hard first before pull for reason of no conflicts.
```
GET .../pull
```
Expected result
```
{"result": "pull completed"}
```
Error
```
{"error": "..."}
```
NOTE: Commit did lost but could be checkout later
---
### Recovery
WIP...

View file

@ -1,5 +0,0 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_prost_build::configure().compile_protos(&["tbm-proto/registry.proto"], &["tbm-proto"])?;
Ok(())
}

1478
src/app.rs

File diff suppressed because it is too large Load diff

View file

@ -1,16 +1,21 @@
use std::{cell::RefCell, collections::HashMap, io::{self, Write}, path::{Path, PathBuf}};
use std::{
cell::RefCell,
collections::HashMap,
io::{self, Write},
path::{Path, PathBuf},
};
use git2::{build::RepoBuilder, Cred, FetchOptions, Progress, RemoteCallbacks};
use log::{info};
use git2::{Cred, FetchOptions, Progress, RemoteCallbacks, build::RepoBuilder};
use log::info;
use crate::gcm;
struct GitState {
progress: Option<Progress<'static>>,
total: usize,
current: usize,
path: Option<PathBuf>,
newline: bool
progress: Option<Progress<'static>>,
total: usize,
current: usize,
path: Option<PathBuf>,
newline: bool,
}
fn print(state: &mut GitState) {
@ -29,10 +34,11 @@ fn print(state: &mut GitState) {
state.newline = true;
}
info!("Resolving deltas {}/{}",
stats.indexed_deltas(),
stats.total_deltas());
info!(
"Resolving deltas {}/{}",
stats.indexed_deltas(),
stats.total_deltas()
);
} else {
info!(
"net {:3}% ({:4} kb, {:5}/{:5}) / idx {:3}% ({:5}/{:5}) \
@ -58,40 +64,38 @@ fn print(state: &mut GitState) {
}
pub fn setup_git_repo(config: HashMap<String, String>) -> gcm::StandardResult {
let state = RefCell::new(GitState {
progress: None,
total: 0,
current: 0,
path: None,
newline: true
});
let state = RefCell::new(GitState {
progress: None,
total: 0,
current: 0,
path: None,
newline: true,
});
let mut cb = RemoteCallbacks::new();
cb.transfer_progress(|stats| {
let mut state = state.borrow_mut();
state.progress = Some(stats.to_owned());
print(&mut state);
true
});
let mut cb = RemoteCallbacks::new();
cb.transfer_progress(|stats| {
let mut state = state.borrow_mut();
state.progress = Some(stats.to_owned());
print(&mut state);
true
});
cb.credentials(|_,_,_| {
Cred::userpass_plaintext(
config.get("GIT_REPO_USERNAME").unwrap_or(&"".to_string()),
config.get("GIT_REPO_PASSWORD").unwrap_or(&"".to_string())
)
});
cb.credentials(|_, _, _| {
Cred::userpass_plaintext(
config.get("GIT_REPO_USERNAME").unwrap_or(&"".to_string()),
config.get("GIT_REPO_PASSWORD").unwrap_or(&"".to_string()),
)
});
let mut fo = FetchOptions::new();
fo.remote_callbacks(cb);
fo.depth(1);
let mut fo = FetchOptions::new();
fo.remote_callbacks(cb);
RepoBuilder::new()
.bare(true)
.fetch_options(fo)
.clone(
config.get("GIT_REPO_REMOTE").unwrap_or(&"".to_string()),
Path::new(config.get("GIT_REPO_LOCAL_DEST").unwrap()).into()
let _ = RepoBuilder::new().bare(false).fetch_options(fo).clone(
config.get("GIT_REPO_REMOTE").unwrap_or(&"".to_string()),
Path::new(config.get("GIT_REPO_LOCAL_DEST").unwrap()).into(),
)?;
Ok(())
}
println!("clone completed !");
Ok(())
}

View file

@ -1,56 +1,64 @@
use std::{fs::File};
use env_logger::Builder;
use libtbr::recipe_functions::common;
use log::info;
use crate::{git::setup_git_repo, reg::{heartbeat_loop, registry::registry_client::RegistryClient}};
use crate::{
git::setup_git_repo,
// reg::{heartbeat_loop, registry::registry_client::RegistryClient},
};
mod app;
mod gcm;
mod git;
mod reg;
// mod reg;
fn setup_log(config: gcm::Configure) -> gcm::StandardResult {
let logfile = File::create(config.get("LOG_NAME").unwrap_or(&"run.log".to_string()))?;
fn setup_log(_config: gcm::Configure) -> gcm::StandardResult {
// NOTE: disable logging file, use send to log service instead
// let logfile = File::create(config.get("LOG_NAME").unwrap_or(&"run.log".to_string()))?;
Builder::from_env(env_logger::Env::default().default_filter_or("debug"))
.target(env_logger::Target::Pipe(Box::new(logfile))).init();
Builder::from_env(env_logger::Env::default().default_filter_or("debug"))
// .target(env_logger::Target::Pipe(Box::new(logfile)))
.init();
Ok(())
Ok(())
}
async fn setup_registry(config: gcm::Configure) -> gcm::StandardResult {
let cfg_clone = config.clone();
let register_server_address = cfg_clone.get("REGISTRY_SERVER").expect("not found registry server");
let name = cfg_clone.get("SERVICE_NAME").expect("missing service name");
let url = cfg_clone.get("SERVICE_URL").expect("missing service url");
let mut client = RegistryClient::connect(register_server_address.clone()).await?;
reg::register_service(&mut client, name, url).await;
tokio::spawn(heartbeat_loop(name.to_string(), url.to_string()));
Ok(())
}
#[tokio::main]
async fn main() -> gcm::StandardResult {
let config: gcm::Configure = common::get_config();
setup_log(config.clone())?;
match std::fs::read_dir(config.get("GIT_REPO_LOCAL_DEST").expect("not exist")) {
Ok(_) => {
info!("GIT_REPO `{dest}` already setup", dest = config.get("GIT_REPO_LOCAL_DEST").unwrap())
},
Err(_) => {
setup_git_repo(config.clone())?;
}
Ok(_) => {
info!(
"GIT_REPO `{dest}` existed, checking if has git",
dest = config.get("GIT_REPO_LOCAL_DEST").unwrap()
);
match std::fs::read_dir(format!(
"{}/.git",
config.get("GIT_REPO_LOCAL_DEST").expect("not exist")
)) {
Ok(_) => {
info!("GIT REPO already set up!")
}
Err(_) => {
setup_git_repo(config.clone())?;
}
}
}
Err(_) => {
setup_git_repo(config.clone())?;
}
}
setup_registry(config.clone()).await?;
info!(
"APP VERSION: {}",
config
.get("CONFIG_VERSION")
.expect("config version not defined")
);
info!("RUNNING: {:?}", config.get("PUBLIC_PORT"));
app::run(config.clone()).await?;
Ok(())

View file

@ -1,5 +1,6 @@
use std::time::Duration;
use log::{error, info};
use tokio::time::sleep;
use tonic::transport::Channel;
@ -17,7 +18,11 @@ pub async fn register_service(client: &mut RegistryClient<Channel>, name: &str,
token: std::env::var("REGISTRY_TOKEN").unwrap_or_default(),
});
let _ = client.register(req).await;
let resp = client.register(req).await;
match resp {
Ok(r) => info!("{:?}", r),
Err(e) => error!("error register: {:?}", e)
}
}
pub async fn heartbeat_loop(name: String, url: String) {