Compare commits

..

No commits in common. "master" and "config" have entirely different histories.

14 changed files with 1269 additions and 3400 deletions

View file

@ -1,5 +0,0 @@
[target.x86_64-unknown-linux-gnu]
linker = "x86_64-linux-gnu-gcc"
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"

View file

@ -1,6 +0,0 @@
/target
*.txt
*.log
*.sh
/recipe_repo

2
.gitignore vendored
View file

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

2666
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -4,29 +4,19 @@ 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://pakin-inspiron-15-3530.tail110d9.ts.net/pakin/libtbr.git", version = "0.1.1" }
libtbr = { git = "https://gitlab.forthrd.io/Pakin/libtbr.git", version = "0.1.1" }
log = "0.4.29"
redis = { version = "1.0.2", features = ["tokio-comp"] }
prost = "0.14.1"
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"] }
tokio-util = { version = "0.7.18", features = ["io"] }
uuid = { version = "1.20.0", features = ["v4"] }
tokio-postgres = "0.7.17"
chrono = "0.4.45"
openssl-sys = { version = "0.9.117", features = ["vendored"] }
tonic = { version = "0.14.2", features = ["transport"] }
tonic-prost = "0.14.2"
[build-dependencies]
tonic-prost-build = "0.14.2"

View file

@ -1,135 +0,0 @@
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM lukemathwalker/cargo-chef:latest-rust-slim-bookworm AS chef
WORKDIR /app
# -----------------------------------------------------------------------
# Stage 1: Prepare the recipe
# -----------------------------------------------------------------------
FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
# -----------------------------------------------------------------------
# Stage 2: Build the dependencies & application
# -----------------------------------------------------------------------
FROM chef AS builder
# Capture Docker's target platform variables
ARG TARGETPLATFORM
ARG TARGETARCH
ARG TARGETVARIANT
# Install host tools needed for compilation (including cmake and clang for aws-lc-sys)
RUN apt-get update && apt-get install -y \
clang \
llvm \
cmake \
make \
pkg-config \
perl \
libssl-dev
# Enable multiarch support so we can download foreign architecture .so and .h files
RUN dpkg --add-architecture amd64 && \
dpkg --add-architecture arm64
# Setup target-specific environment variables manually based on target architecture
RUN apt-get update && \
if [ "$TARGETARCH" = "amd64" ]; then \
apt-get install -y gcc-x86-64-linux-gnu g++-x86-64-linux-gnu libssl-dev; \
echo "TARGET_TRIPLE=x86_64-unknown-linux-gnu" >> /env_config; \
echo "CC_x86_64_unknown_linux_gnu=/usr/bin/x86_64-linux-gnu-gcc" >> /env_config; \
echo "CXX_x86_64_unknown_linux_gnu=/usr/bin/x86_64-linux-gnu-g++" >> /env_config; \
echo "CC=/usr/bin/x86_64-linux-gnu-gcc" >> /env_config; \
echo "CXX=/usr/bin/x86_64-linux-gnu-g++" >> /env_config; \
echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=/usr/bin/x86_64-linux-gnu-gcc" >> /env_config; \
echo "OPENSSL_DIR=/usr" >> /env_config; \
if [ "$TARGETVARIANT" = "v3" ]; then \
echo "export RUSTFLAGS='-C target-cpu=x86-64-v3'" >> /env_config; \
fi \
elif [ "$TARGETARCH" = "arm64" ]; then \
apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu libssl-dev; \
echo "TARGET_TRIPLE=aarch64-unknown-linux-gnu" >> /env_config; \
echo "CC_aarch64_unknown_linux_gnu=/usr/bin/aarch64-linux-gnu-gcc" >> /env_config; \
echo "CXX_aarch64_unknown_linux_gnu=/usr/bin/aarch64-linux-gnu-g++" >> /env_config; \
echo "CC=/usr/bin/aarch64-linux-gnu-gcc" >> /env_config; \
echo "CXX=/usr/bin/aarch64-linux-gnu-g++" >> /env_config; \
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=/usr/bin/aarch64-linux-gnu-gcc" >> /env_config; \
echo "OPENSSL_DIR=/usr" >> /env_config; \
fi
# Force openssl-sys to download, compile, and statically link OpenSSL safely
ENV OPENSSL_STATIC=1
ENV OPENSSL_VENDED=1
# Tell cargo to allow cross-compiling build scripts
ENV PKG_CONFIG_ALLOW_CROSS=1
# Load the environment configurations and download Rust target targets
RUN . /env_config && \
rustup target add "$TARGET_TRIPLE"
# Tell aws-lc-sys exactly how to build via CMake
ENV AWS_LC_SYS_CMAKE_BUILDER=1
ENV AWS_LC_SYS_PREBUILT_NASM=1
COPY .cargo /app/.cargo
# Cache and build only the dependencies (the chef recipe)
COPY --from=planner /app/recipe.json recipe.json
RUN . /env_config && \
cargo chef cook --release --target "$TARGET_TRIPLE" --recipe-path recipe.json
# Copy actual source code
COPY . .
# Build the main application using cached dependencies
RUN . /env_config && \
cargo build --release --target "$TARGET_TRIPLE" && \
cp target/${TARGET_TRIPLE}/release/tbm-git-repo-service /tbm-git-repo-service
# -----------------------------------------------------------------------
# Stage 3: Minimal Runtime
# -----------------------------------------------------------------------
# We dynamically anchor the platform to a base architecture to stop Docker
# from panicking over micro-variants like v3 during runtime container setup.
FROM --platform=$TARGETPLATFORM debian:bookworm-slim AS runtime-base
# Trick Buildx: force the runner to resolve back to basic broad platforms
# so it can execute native container processes like apt-get without errors.
FROM --platform=linux/amd64 debian:bookworm-slim AS runtime-amd64
FROM --platform=linux/arm64 debian:bookworm-slim AS runtime-arm64
# Select the clean runtime environment matching your target layout
FROM runtime-$TARGETARCH AS final-runtime
WORKDIR /app
ENV TZ=Asia/Bangkok
# Install runtime dependencies if needed (like ca-certificates or openssl)
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
libssl3 \
zlib1g \
curl \
pkg-config \
ca-certificates \
openssh-client \
tzdata && ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /tbm-git-repo-service /app/tbm-git-repo-service
COPY --from=builder /app/.tbcfg /app/.tbcfg
RUN mkdir -p /root/.ssh && \
ssh-keyscan 192.168.10.159 >> /root/.ssh/known_hosts
# RUN cp /usr/src/app/target/release/tbm-git-repo-service /usr/src/app/
# RUN cp /usr/src/app/.tbcfg /usr/src/app/
# RUN rm -rf /usr/src/app/target
EXPOSE 36583
CMD ["./tbm-git-repo-service"]

183
README.md
View file

@ -1,183 +0,0 @@
# 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...

5
build.rs Normal file
View file

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

View file

@ -1 +0,0 @@
docker build --pull --platform linux/amd64,linux/arm64 -t pakin-inspiron-15-3530.tail110d9.ts.net/pakin/recipe-repo .

View file

@ -1 +0,0 @@
docker push pakin-inspiron-15-3530.tail110d9.ts.net/pakin/server-m2:latest

1468
src/app.rs

File diff suppressed because it is too large Load diff

View file

@ -1,21 +1,16 @@
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::{Cred, FetchOptions, Progress, RemoteCallbacks, build::RepoBuilder};
use log::info;
use git2::{build::RepoBuilder, Cred, FetchOptions, Progress, RemoteCallbacks};
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) {
@ -34,11 +29,10 @@ 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}) \
@ -64,38 +58,40 @@ 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);
let mut fo = FetchOptions::new();
fo.remote_callbacks(cb);
fo.depth(1);
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(),
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()
)?;
println!("clone completed !");
Ok(())
Ok(())
}

View file

@ -1,64 +1,56 @@
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 {
// NOTE: disable logging file, use send to log service instead
// let logfile = File::create(config.get("LOG_NAME").unwrap_or(&"run.log".to_string()))?;
fn setup_log(config: gcm::Configure) -> gcm::StandardResult {
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}` 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())?;
}
Ok(_) => {
info!("GIT_REPO `{dest}` already setup", dest = config.get("GIT_REPO_LOCAL_DEST").unwrap())
},
Err(_) => {
setup_git_repo(config.clone())?;
}
}
info!(
"APP VERSION: {}",
config
.get("CONFIG_VERSION")
.expect("config version not defined")
);
info!("RUNNING: {:?}", config.get("PUBLIC_PORT"));
setup_registry(config.clone()).await?;
app::run(config.clone()).await?;
Ok(())

View file

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