feat: initial project commit

commit to push first working snapshot to codeberg
This commit is contained in:
Patryk Hegenberg 2025-03-15 00:13:20 +01:00
commit 09b1054588
25 changed files with 1405 additions and 0 deletions

104
README.md Normal file
View file

@ -0,0 +1,104 @@
# JakartaEE & Spring Boot Starter
This application is a GUI tool developed in Go using the Fyne framework (v2.5.4). It helps users set up a development environment for JakartaEE and Spring Boot projects by checking and installing necessary dependencies, and then allowing users to deploy starter projects.
## Features
- *Dependency Check*: Verifies the installation of required tools (Visual Studio Code and Docker).
- *Automatic Installation*: Offers to install missing dependencies on supported platforms.
- *Project Selection*: Provides a list of starter projects for JakartaEE and Spring Boot.
- *Project Deployment*: Allows users to deploy selected projects and open them in Visual Studio Code with dev containers.
## Prerequisites
- Go 1.23 or later
- Fyne v2.5.4
## Installation
### Option 1: Download Pre-built Binary
Go to the Releases page of this repository.
Download the appropriate binary for your operating system.
### Option 2: Build from Source
- Clone the repository:
```bash
git clone https://github.com/yourusername/jakartaee-springboot-starter.git
```
Navigate to the project directory:
```bash
cd jakartaee-springboot-starter
```
Install dependencies:
The list of dependencies required for the development of Fyne Apps can be found at “https://docs.fyne.io/started/”.
```bash
go mod tidy
```
Build the application:
```bash
go build
```
## Usage
Run the application:
If you downloaded a pre-built binary:
```bash
./jakartaee-springboot-starter
```
or by double-clicking the excutable.
If you built from source:
```bash
./jakartaee-springboot-starter
```
The application will launch a GUI window where you can:
1. Check and install dependencies (Visual Studio Code and Docker).
2. Select a starter project from the available options.
3. Deploy the selected project and open it in Visual Studio Code.
Project Structure
// TODO
- main.go: Contains the entire application logic, including GUI setup, dependency checking, and project deployment.
- projects/: Directory containing starter project templates.
- projects.json: Configuration file for additional plugin projects.
## Customization
To add new starter projects:
1. Create a new project template in the projects/ directory.
2. Add the project details to the projects.json file.
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
[Insert your chosen license here]
## Acknowledgments
- Fyne: https://fyne.io/
- JakartaEE: https://jakarta.ee/
- Spring Boot: https://spring.io/projects/spring-boot

2
cmd/jws/FyneApp.toml Normal file
View file

@ -0,0 +1,2 @@
[Details]
Build = 4

BIN
cmd/jws/Icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

113
cmd/jws/main.go Normal file
View file

@ -0,0 +1,113 @@
// cmd/jws/main.go
package main
import (
"embed"
"encoding/json"
"fmt"
"jws/internal/dependency"
"jws/internal/gui"
"jws/internal/project"
"os"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/theme"
)
//go:embed all:projects
var projectsFS embed.FS
var (
dependencies []dependency.Dependency
projects []project.Project
mainWindow fyne.Window
)
func main() {
a := app.New()
mainWindow = a.NewWindow("JakartaEE & Spring Boot Starter")
mainWindow.Resize(fyne.NewSize(600, 600))
mainWindow.SetFixedSize(true)
// Initialize dependencies
dependencies = []dependency.Dependency{
{Name: "Visual Studio Code", Installed: false, Icon: theme.DocumentIcon()},
{Name: "Docker", Installed: false, Icon: theme.MediaPlayIcon()},
}
// Standard projects
standartProjects := []project.Project{
{
Name: "JakartaEE Todo-App mit Servlet",
Description: "Eine Todo-Anwendung mit Jakarta Servlet und\nPostgreSQL-Datenbank.",
FolderName: "jakarta-servlet-todo",
},
{
Name: "JakartaEE Todo-App mit JSP",
Description: "Eine Todo-Anwendung mit Jakarta Server Pages und PostgreSQL-Datenbank.",
FolderName: "jakarta-jsp-todo",
},
{
Name: "JakartaEE Todo-App als REST-API",
Description: "Eine Todo-Anwendung als REST-API mit JakartaEE und PostgreSQL-Datenbank.",
FolderName: "jakarta-rest-todo",
},
{
Name: "Spring Boot Todo-App als Microservice",
Description: "Eine Todo-Anwendung als Microservice mit Spring Boot und PostgreSQL-Datenbank.",
FolderName: "spring-boot-todo",
},
}
// Load additional projects from projects.json
pluginProjects, err := loadProjects("projects.json")
if err != nil {
dialog.ShowError(fmt.Errorf("error loading projects: %v", err), mainWindow)
return
}
projects = append(standartProjects, pluginProjects...)
// Initialize GUI package
gui.Init(mainWindow, dependencies, projects, projectsFS)
// Check dependencies
dependency.CheckDependencies(dependencies)
allInstalled := true
for _, dep := range dependencies {
if !dep.Installed {
allInstalled = false
break
}
}
// Show appropriate screen
if allInstalled {
gui.ShowProjectScreen()
} else {
gui.ShowDependencyScreen()
}
mainWindow.ShowAndRun()
}
func loadProjects(filePath string) ([]project.Project, error) {
file, err := os.Open(filePath)
if err != nil {
if os.IsNotExist(err) {
return []project.Project{}, nil
}
return nil, fmt.Errorf("error opening JSON-file: %v", err)
}
defer file.Close()
var projects []project.Project
decoder := json.NewDecoder(file)
if err := decoder.Decode(&projects); err != nil {
return nil, fmt.Errorf("error decoding JSON-file: %v", err)
}
return projects, nil
}

View file

@ -0,0 +1,18 @@
FROM mcr.microsoft.com/devcontainers/java:1-21-bullseye
ARG INSTALL_MAVEN="false"
ARG MAVEN_VERSION=""
ARG INSTALL_GRADLE="false"
ARG GRADLE_VERSION=""
RUN apt-get update && apt-get upgrade -y && \
apt-get install -y \
git
RUN if [ "${INSTALL_MAVEN}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install maven \"${MAVEN_VERSION}\""; fi \
&& if [ "${INSTALL_GRADLE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install gradle \"${GRADLE_VERSION}\""; fi
RUN mkdir -p /workspaces/${localWorkspaceFolderBasename} && chown -R vscode:vscode /workspaces/${localWorkspaceFolderBasename}
ENTRYPOINT ["/bin/bash"]

View file

@ -0,0 +1,9 @@
{
"name": "Jakarta EE Todo App & PostgreSQL",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces",
"extensions": ["vscjava.vscode-java-pack", "redhat.vscode-xml"],
"forwardPorts": [9080],
"remoteUser": "vscode"
}

View file

@ -0,0 +1,32 @@
version: "3.8"
volumes:
postgres-data:
services:
app:
container_name: javadev
build:
context: .
dockerfile: Dockerfile
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: postgres
POSTGRES_HOSTNAME: postgresdb
volumes:
- ..:/workspace
command: sleep infinity
network_mode: service:db
stdin_open: true
db:
container_name: postgresdb
image: postgres:latest
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: postgres

View file

@ -0,0 +1,49 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>jakarta-servlet-todo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<jakartaee.version>10.0.0</jakartaee.version>
<hibernate.version>6.0.0.Final</hibernate.version>
<postgresql.version>42.3.1</postgresql.version>
</properties>
<dependencies>
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-api</artifactId>
<version>${jakartaee.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgresql.version}</version>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,36 @@
package com.example.dao;
import com.example.model.Todo;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.transaction.Transactional;
import java.util.List;
public class TodoDAO {
@PersistenceContext
private EntityManager em;
@Transactional
public void create(Todo todo) {
em.persist(todo);
}
public List<Todo> findAll() {
return em.createQuery("SELECT t FROM Todo t", Todo.class).getResultList();
}
@Transactional
public void update(Todo todo) {
em.merge(todo);
}
@Transactional
public void delete(Long id) {
Todo todo = em.find(Todo.class, id);
if (todo != null) {
em.remove(todo);
}
}
}

View file

@ -0,0 +1,43 @@
package com.example.model;
import jakarta.persistence.*;
@Entity
@Table(name = "todos")
public class Todo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private boolean completed;
// Getter und Setter
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public boolean isCompleted() {
return completed;
}
public void setCompleted(boolean completed) {
this.completed = completed;
}
}

View file

@ -0,0 +1,40 @@
package com.example.servlet;
import com.example.dao.TodoDAO;
import com.example.model.Todo;
import jakarta.inject.Inject;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
@WebServlet("/todos")
public class TodoServlet extends HttpServlet {
@Inject
private TodoDAO todoDAO;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
List<Todo> todos = todoDAO.findAll();
req.setAttribute("todos", todos);
req.getRequestDispatcher("/index.jsp").forward(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String title = req.getParameter("title");
boolean completed = "on".equals(req.getParameter("completed"));
Todo todo = new Todo();
todo.setTitle(title);
todo.setCompleted(completed);
todoDAO.create(todo);
resp.sendRedirect(req.getContextPath() + "/todos");
}
}

View file

@ -0,0 +1,19 @@
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
version="3.0">
<persistence-unit name="todoPU">
<class>com.example.model.Todo</class>
<properties>
<property name="jakarta.persistence.jdbc.driver" value="org.postgresql.Driver"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:postgresql://db:5432/tododb"/>
<property name="jakarta.persistence.jdbc.user" value="todouser"/>
<property name="jakarta.persistence.jdbc.password" value="todopass"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
</properties>
</persistence-unit>
</persistence>

View file

@ -0,0 +1,9 @@
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<display-name>JakartaEE Todo App</display-name>
</web-app>

40
go.mod Normal file
View file

@ -0,0 +1,40 @@
module jws
go 1.23.5
require (
fyne.io/fyne/v2 v2.6.0-alpha1
fyne.io/tools v1.0.0-alpha1
)
require (
fyne.io/systray v1.11.0 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fredbi/uri v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fyne-io/gl-js v0.1.0 // indirect
github.com/fyne-io/glfw-js v0.1.0 // indirect
github.com/fyne-io/image v0.1.0 // indirect
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
github.com/go-text/render v0.2.0 // indirect
github.com/go-text/typesetting v0.2.1 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rymdport/portal v0.3.0 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/yuin/goldmark v1.7.8 // indirect
golang.org/x/image v0.24.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

76
go.sum Normal file
View file

@ -0,0 +1,76 @@
fyne.io/fyne/v2 v2.6.0-alpha1 h1:ALx1JJDdCYQpm5RS7CIK9bam9H7hddSDuRy/fyj9pb8=
fyne.io/fyne/v2 v2.6.0-alpha1/go.mod h1:Bzv2yK+ncZ8LJbHKjyJJpEAFlbs6oulHgKm04ObOqA8=
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
fyne.io/tools v1.0.0-alpha1 h1:350eF+LVPbIHNgYPCoP04yCsQSFQ3aupATx33ClZjZk=
fyne.io/tools v1.0.0-alpha1/go.mod h1:7gcHTl85tD/yLcGcU2bJGECAwSVAWrr99Ngmgrz/s54=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8=
github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fyne-io/gl-js v0.1.0 h1:8luJzNs0ntEAJo+8x8kfUOXujUlP8gB3QMOxO2mUdpM=
github.com/fyne-io/gl-js v0.1.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
github.com/fyne-io/glfw-js v0.1.0 h1:RGGMmVjcsG17Oifl3X2UJ5vH3PgS4B1UY3ASeN5BXbI=
github.com/fyne-io/glfw-js v0.1.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
github.com/fyne-io/image v0.1.0 h1:Vm2TQJ2PWGHCf3jYi1/XroaNNMu+GfI/O2QpSbZd4XQ=
github.com/fyne-io/image v0.1.0/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 h1:wMeVzrPO3mfHIWLZtDcSaGAe2I4PW9B/P5nMkRSwCAc=
github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
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/rymdport/portal v0.3.0 h1:QRHcwKwx3kY5JTQcsVhmhC3TGqGQb9LFghVNUy8AdB8=
github.com/rymdport/portal v0.3.0/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -0,0 +1,315 @@
package dependency
import (
"fmt"
osinfo "jws/internal/os"
"jws/pkg/download"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
"fyne.io/tools"
)
type Dependency struct {
Name string
Installed bool
Icon fyne.Resource
}
func CheckDependencies(dependencies []Dependency) {
// Check VSCode
dependencies[0].Installed = checkVSCode()
// Check Docker
if runtime.GOOS == "windows" {
dependencies[1].Installed = checkDockerDesktop()
} else {
dependencies[1].Installed = checkDocker()
}
}
func checkVSCode() bool {
switch runtime.GOOS {
case "windows":
_, err := os.Stat(filepath.Join(os.Getenv("LOCALAPPDATA"), "Programs", "Microsoft VS Code", "Code.exe"))
if err == nil {
return true
}
_, err = os.Stat(filepath.Join(os.Getenv("ProgramFiles"), "Microsoft VS Code", "Code.exe"))
return err == nil
case "darwin":
_, err := os.Stat("/Applications/Visual Studio Code.app")
if err != nil {
cmd := tools.CommandInShell("which", "code")
return cmd.Run() == nil
}
return err == nil
case "linux":
cmd := tools.CommandInShell("which", "code")
return cmd.Run() == nil
default:
return false
}
}
func checkDocker() bool {
cmd := tools.CommandInShell("which", "docker")
return cmd.Run() == nil
}
func checkDockerDesktop() bool {
switch runtime.GOOS {
case "windows":
_, err := os.Stat(filepath.Join(os.Getenv("ProgramFiles"), "Docker", "Docker", "Docker Desktop.exe"))
return err == nil
case "darwin":
_, err := os.Stat("/Applications/Docker.app")
return err == nil
case "linux":
cmd := tools.CommandInShell("systemctl", "is-active", "docker")
output, _ := cmd.Output()
return strings.TrimSpace(string(output)) == "active"
default:
return false
}
}
func InstallDependency(index int, sudoPassword string, dependencies []Dependency, mainWindow fyne.Window) {
depName := dependencies[index].Name
var cmd *exec.Cmd
var err error
switch runtime.GOOS {
case "windows":
switch index {
case 0: // VSCode
cmd = tools.CommandInShell("winget", "install", "-e", "--id",
"Microsoft.VisualStudioCode")
case 1: // Docker Desktop
wslCheckCmd := tools.CommandInShell("wsl", "--status")
err := wslCheckCmd.Run()
if err != nil {
wslInstallCmd := tools.CommandInShell("wsl", "--install")
err = wslInstallCmd.Run()
if err != nil {
dialog.ShowError(fmt.Errorf("error: installing WSL: %v", err), mainWindow)
return
}
dialog.ShowInformation("WSL wird installiert", "WSL wird installiert. Bitte warten Sie, bis die Installation abgeschlossen ist und starten Sie die Anwendung neu.", mainWindow)
return
}
cmd = tools.CommandInShell("winget", "install", "-e", "--id",
"Docker.DockerDesktop")
}
case "darwin":
brewCheckCmd := tools.CommandInShell("which", "brew")
err := brewCheckCmd.Run()
if err != nil {
brewInstallCmd := tools.CommandInShell("bin/bash", "-c", "\"$(curl -fsSl https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"")
err := brewInstallCmd.Run()
if err != nil {
dialog.ShowError(fmt.Errorf("error: installing homebrew falied: %v", err), mainWindow)
}
}
switch index {
case 0: // VSCode
cmd = tools.CommandInShell("brew", "install", "--cask", "visual-studio-code")
case 1: // Docker
cmd = tools.CommandInShell("brew", "install", "docker")
}
case "linux":
osInfo, err := osinfo.GetLinuxDistribution()
if err != nil {
log.Println(err)
dialog.ShowError(fmt.Errorf("error getting OS info: %v", err), mainWindow)
return
}
var downloadURL, fileName string
switch index {
case 0: // VSCode
switch osInfo.ID {
case "debian", "ubuntu", "linuxmint":
downloadURL = "https://code.visualstudio.com/sha/download?build=stable&os=linux-deb-x64"
fileName = "vscode.deb"
case "fedora":
downloadURL = "https://code.visualstudio.com/sha/download?build=stable&os=linux-rpm-x64"
fileName = "vscode.rpm"
default:
dialog.ShowInformation("Nicht unterstützt", fmt.Sprintf("Automatische Installation für dieses OS %v nicht verfügbar", osInfo), mainWindow)
return
}
go func() {
progressBar := widget.NewProgressBar()
progressDialog := dialog.NewCustomWithoutButtons("Download in progress", progressBar, mainWindow)
progressDialog.Show()
homeDir, _ := os.UserHomeDir()
downloadDir := filepath.Join(homeDir, "Downloads")
filePath := filepath.Join(downloadDir, fileName)
err := download.WithProgressBar(downloadURL, filePath, progressBar)
progressDialog.Hide()
if err != nil {
dialog.ShowError(err, mainWindow)
return
}
var installCmd string
switch osInfo.ID {
case "debian", "ubuntu", "linuxmint":
installCmd = fmt.Sprintf("sudo -S dpkg -i %s", filePath)
case "fedora":
installCmd = fmt.Sprintf("sudo -S dnf install -y %s", filePath)
}
cmd := exec.Command("sh", "-c", installCmd)
cmd.Stdin = strings.NewReader(sudoPassword + "\n")
dialog.ShowInformation("Installation gestartet",
"Die Installation von VSCode wurde gestartet.", mainWindow)
output, err := cmd.CombinedOutput()
if err != nil {
dialog.ShowError(fmt.Errorf("installation failed:\n%s", output), mainWindow)
return
} else {
dialog.ShowInformation("Erfolg", "VSCode erfolgreich installiert!", mainWindow)
CheckDependencies(dependencies)
}
}()
case 1: // Docker
go func() {
progressBar := widget.NewProgressBar()
progressDialog := dialog.NewCustomWithoutButtons("Docker Installation läuft...", progressBar, mainWindow)
progressDialog.Show()
osInfo, err := osinfo.GetLinuxDistribution()
if err != nil {
progressDialog.Hide()
dialog.ShowError(fmt.Errorf("error getting os infos: %v", err), mainWindow)
return
}
var commands []string
var cleanupCommands []string
var totalSteps int
switch osInfo.ID {
case "ubuntu", "linuxmint", "debian":
// Ubuntu/Debian Commands
distroPath := "ubuntu"
codeName := "$(. /etc/os-release && echo \"${UBUNTU_CODENAME:-$VERSION_CODENAME}\")"
if osInfo.ID == "debian" {
distroPath = "debian"
codeName = "$(. /etc/os-release && echo \"$VERSION_CODENAME\")"
}
arch := "$(dpkg --print-architecture)"
commands = []string{
"apt-get update",
"apt-get install -y wget",
fmt.Sprintf("wget -qO- https://download.docker.com/linux/%s/dists/%s/pool/stable/%s/ | grep -oP 'href=\"\\K[^\"]*(?=.*deb)' | xargs -I{} wget https://download.docker.com/linux/%s/dists/%s/pool/stable/%s/{}",
distroPath, codeName, arch, distroPath, codeName, arch),
"dpkg -i ./containerd.io*.deb docker-ce*.deb docker-ce-cli*.deb docker-buildx-plugin*.deb docker-compose-plugin*.deb",
"apt-get install -f -y",
"service docker start",
}
cleanupCommands = []string{
"rm -rf ./containerd.io*.deb ./docker-ce*.deb ./docker-ce-cli*.deb ./docker-buildx-plugin*.deb ./docker-compose-plugin*.deb",
"apt-get autoremove -y",
"apt-get clean",
}
totalSteps = len(commands) + len(cleanupCommands)
case "fedora":
// Fedora Commands
fedoraVer := "$(rpm -E %fedora)"
commands = []string{
"dnf install -y wget",
fmt.Sprintf("wget https://download.docker.com/linux/fedora/%s/x86_64/stable/Packages/containerd-*.rpm docker-*.rpm docker-ce-*.rpm", fedoraVer),
"dnf install -y ./*.rpm",
"systemctl enable --now docker",
}
cleanupCommands = []string{
"rm -rf ./containerd-*.rpm ./docker-*.rpm ./docker-ce-*.rpm",
"dnf autoremove -y",
"dnf clean all",
}
totalSteps = len(commands) + len(cleanupCommands)
default:
progressDialog.Hide()
dialog.ShowInformation("not supported", "Automatic Docker installation not supported for your OS.", mainWindow)
return
}
progressStep := 1.0 / float64(totalSteps)
currentProgress := 0.0
for _, cmd := range commands {
command := exec.Command("sudo", "-S", "sh", "-c", cmd)
command.Stdin = strings.NewReader(sudoPassword + "\n")
if output, err := command.CombinedOutput(); err != nil {
progressDialog.Hide()
dialog.ShowError(fmt.Errorf("error at %s:\n%s", cmd, output), mainWindow)
return
}
currentProgress += progressStep
progressBar.SetValue(currentProgress)
}
for _, cmd := range cleanupCommands {
command := exec.Command("sudo", "-S", "sh", "-c", cmd)
command.Stdin = strings.NewReader(sudoPassword + "\n")
if output, err := command.CombinedOutput(); err != nil {
progressDialog.Hide()
dialog.ShowError(fmt.Errorf("cleanup error at %s:\n%s", cmd, output), mainWindow)
return
}
currentProgress += progressStep
progressBar.SetValue(currentProgress)
}
progressDialog.Hide()
dialog.ShowInformation(
"Installation finished",
"Docker was succesfully installed! Please re-start your system to let the changes take effect.",
mainWindow,
)
CheckDependencies(dependencies)
}()
}
}
if cmd != nil {
err = cmd.Start()
if err != nil {
dialog.ShowError(fmt.Errorf("error starting installation process: %v", err), mainWindow)
} else {
dialog.ShowInformation("Installation started",
fmt.Sprintf("Installation of %s was started", depName), mainWindow)
}
}
}

View file

@ -0,0 +1,92 @@
package gui
import (
"jws/internal/dependency"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
// ShowDependencyScreen displays the dependency check screen
func ShowDependencyScreen() {
title := widget.NewLabel("check dependencies")
title.TextStyle = fyne.TextStyle{Bold: true}
title.Alignment = fyne.TextAlignCenter
description := widget.NewLabel("dependencies needed for dev-environment:")
description.Wrapping = fyne.TextWrapWord
dependencyContainer := container.NewVBox()
allInstalled := true
for i, dep := range dependencies {
statusIcon := theme.CancelIcon()
statusText := "not installed"
if dep.Installed {
statusIcon = theme.ConfirmIcon()
statusText = "installed"
} else {
allInstalled = false
}
depBox := container.NewHBox(
widget.NewIcon(dep.Icon),
widget.NewLabel(dep.Name),
layout.NewSpacer(),
widget.NewIcon(statusIcon),
widget.NewLabel(statusText),
)
dependencyContainer.Add(depBox)
if !dep.Installed {
installBtn := widget.NewButton("install", func(i int) func() {
return func() {
ShowPasswordDialog(i)
}
}(i))
depBox = container.NewStack(
layout.NewSpacer(),
installBtn,
)
dependencyContainer.Add(depBox)
}
}
nextBtn := widget.NewButton("go to projects", func() {
if allInstalled {
ShowProjectScreen()
} else {
dialog.ShowInformation("dependencies missing",
"Bitte installieren Sie alle erforderlichen Abhängigkeiten, bevor Sie fortfahren.", mainWindow)
}
})
nextBtn.Importance = widget.HighImportance
recheckBtn := widget.NewButton("check again", func() {
dependency.CheckDependencies(dependencies)
ShowDependencyScreen()
})
buttonContainer := container.New(layout.NewGridLayout(2), recheckBtn, nextBtn)
paddedButtonContainer := container.NewPadded(buttonContainer)
description.Alignment = fyne.TextAlignCenter
header := container.NewVBox(title, description)
content := container.NewBorder(header,
paddedButtonContainer,
nil,
nil,
dependencyContainer,
)
mainWindow.SetContent(content)
}

47
internal/gui/gui.go Normal file
View file

@ -0,0 +1,47 @@
package gui
import (
"embed"
"jws/internal/dependency"
"jws/internal/project"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
)
var (
// Global variables available to the package
mainWindow fyne.Window
dependencies []dependency.Dependency
projects []project.Project
projectsFS embed.FS
)
// Init initializes the GUI package with required dependencies
func Init(window fyne.Window, deps []dependency.Dependency, projs []project.Project, fs embed.FS) {
mainWindow = window
dependencies = deps
projects = projs
projectsFS = fs
}
// ShowPasswordDialog shows a dialog to enter sudo password for installations
func ShowPasswordDialog(index int) {
password := binding.NewString()
entry := widget.NewEntryWithData(password)
entry.Password = true
dialog.NewForm("Sudo Password", "ok", "cancel",
[]*widget.FormItem{widget.NewFormItem("password", entry)},
func(b bool) {
pass, err := password.Get()
if err == nil {
if b {
dependency.InstallDependency(index, pass, dependencies, mainWindow)
}
}
},
mainWindow).Show()
}

View file

@ -0,0 +1,91 @@
package gui
import (
"jws/internal/project"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
// ShowProjectScreen displays the project selection screen
func ShowProjectScreen() {
title := widget.NewLabel("Project-Selection")
title.TextStyle = fyne.TextStyle{Bold: true}
title.Alignment = fyne.TextAlignCenter
description := widget.NewLabel("Choose a Starter-Project to deploy:")
description.Wrapping = fyne.TextWrapWord
description.Alignment = fyne.TextAlignCenter
projectsList := container.NewVBox()
for i, proj := range projects {
projTitle := widget.NewLabel(proj.Name)
projTitle.TextStyle = fyne.TextStyle{Bold: true}
projDesc := widget.NewLabel(proj.Description)
projDesc.Wrapping = fyne.TextWrapWord
deployBtn := widget.NewButton("deploy", func(i int) func() {
return func() {
project.DeployProject(i, projects, projectsFS, mainWindow)
}
}(i))
deployBtn.Importance = widget.HighImportance
projectContent := container.NewBorder(
nil,
nil,
nil,
container.NewCenter(deployBtn),
container.NewVBox(projTitle, projDesc),
)
projectCard := container.NewBorder(
nil,
nil,
nil,
nil,
container.NewPadded(projectContent),
)
bg := canvas.NewRectangle(theme.Color(theme.ColorNameBackground))
borderedContainer := container.NewStack(
bg,
projectCard,
)
spacedContainer := container.NewVBox(
borderedContainer,
widget.NewSeparator(),
)
projectsList.Add(spacedContainer)
}
backBtn := widget.NewButton("Check Dependecies", func() {
ShowDependencyScreen()
})
scrollContainer := container.NewVScroll(projectsList)
header := container.NewVBox(
title,
description,
widget.NewSeparator(),
)
content := container.NewBorder(
header,
backBtn, // footer,
nil,
nil,
scrollContainer,
)
mainWindow.SetContent(content)
}

83
internal/os/osinfo.go Normal file
View file

@ -0,0 +1,83 @@
package os
import (
"fmt"
"log"
"os"
"os/exec"
"strings"
)
type OS struct {
ID string
Name string
Version string
PackageManager string
}
func GetLinuxDistribution() (*OS, error) {
_, err := os.Stat("/etc/os-release")
if os.IsNotExist(err) {
return nil, fmt.Errorf("unable to read system information")
}
osRelease, _ := os.ReadFile("/etc/os-release")
return parseOsRelease(string(osRelease)), nil
}
func parseOsRelease(osRelease string) *OS {
var result OS
result.ID = "Unknown"
result.Name = "Unknown"
result.Version = "Unknown"
lines := strings.Split(osRelease, "\n")
for _, line := range lines {
splitLine := strings.SplitN(line, "=", 2)
if len(splitLine) != 2 {
continue
}
switch splitLine[0] {
case "ID":
result.ID = strings.ToLower(strings.Trim(splitLine[1], "\""))
case "NAME":
result.Name = strings.Trim(splitLine[1], "\"")
case "VERSION_ID":
result.Version = strings.Trim(splitLine[1], "\"")
}
}
err := result.getPackageManager()
if err != nil {
log.Fatal(err)
}
return &result
}
func (os *OS) getPackageManager() error {
switch os.ID {
case "debian", "ubuntu", "linuxmint":
os.PackageManager = "apt"
return nil
case "arch":
os.PackageManager = "pacman"
return nil
case "fedora":
os.PackageManager = "dnf"
return nil
default:
pmcommands := []string{
"apt",
"dnf",
"pacman",
}
for _, pmname := range pmcommands {
_, err := exec.LookPath(pmname)
if err == nil {
os.PackageManager = pmname
return nil
}
}
return fmt.Errorf("no packagemanager found for os: %s", os.Name)
}
}

127
internal/project/project.go Normal file
View file

@ -0,0 +1,127 @@
package project
import (
"embed"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"runtime"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
)
type Project struct {
Name string `json:"name"`
Description string `json:"description"`
FolderName string `json:"folderName"`
}
func DeployProject(index int, projects []Project, projectsFS embed.FS, mainWindow fyne.Window) {
project := projects[index]
homeDir, err := os.UserHomeDir()
if err != nil {
dialog.ShowError(fmt.Errorf("error getting home directory: %v", err), mainWindow)
return
}
projectPath := filepath.Join(homeDir, "Projects", project.FolderName)
confirmDialog := dialog.NewConfirm(
"deploying project",
fmt.Sprintf("the project '%s' will be deployed to '%s' and opend in VS Code. Continue?", project.Name, projectPath),
func(ok bool) {
if ok {
progress := widget.NewProgressBar()
progressDiag := dialog.NewCustomWithoutButtons("project will be deployed", progress, mainWindow)
progressDiag.Show()
go func() {
defer progressDiag.Hide()
progress.SetValue(0.1)
err := os.MkdirAll(projectPath, 0755)
if err != nil {
dialog.ShowError(fmt.Errorf("error creating directory: %v", err), mainWindow)
return
}
progress.SetValue(0.3)
err = CopyEmbeddedProject(projectsFS, project.FolderName, projectPath)
if err != nil {
dialog.ShowError(fmt.Errorf("error copying project files: %v", err), mainWindow)
return
}
progress.SetValue(0.7)
var openCmd *exec.Cmd
switch runtime.GOOS {
case "windows":
openCmd = exec.Command("code", projectPath)
case "darwin":
openCmd = exec.Command("open", "-a", "Visual Studio Code", projectPath)
case "linux":
openCmd = exec.Command("code", projectPath)
}
if openCmd != nil {
progress.SetValue(0.9)
err = openCmd.Run()
if err != nil {
dialog.ShowError(fmt.Errorf("error opening VS Code: %v", err), mainWindow)
return
}
}
progress.SetValue(1.0)
dialog.ShowInformation("project deployed",
fmt.Sprintf("the project '%s' was successfully deployed and opend in VS Code.\n\n"+
"Path: %s\n\n"+
"Application can be started with Docker Compose.",
project.Name, projectPath), mainWindow)
}()
}
},
mainWindow,
)
confirmDialog.Show()
}
func CopyEmbeddedProject(projectsFS embed.FS, projectFolder string, targetPath string) error {
sourcePath := fmt.Sprintf("projects/%s", projectFolder)
return fs.WalkDir(projectsFS, sourcePath, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(sourcePath, path)
if err != nil {
return err
}
destPath := filepath.Join(targetPath, relPath)
if d.IsDir() {
return os.MkdirAll(destPath, 0755)
}
data, err := projectsFS.ReadFile(path)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
return err
}
return os.WriteFile(destPath, data, 0644)
})
}

60
pkg/download/download.go Normal file
View file

@ -0,0 +1,60 @@
package download
import (
"fmt"
"io"
"net/http"
"os"
"time"
"fyne.io/fyne/v2/widget"
)
func WithProgressBar(downloadURL, filePath string, progressBar *widget.ProgressBar) error {
client := &http.Client{
Timeout: 15 * time.Minute,
}
resp, err := client.Get(downloadURL)
if err != nil {
return fmt.Errorf("download failed: %v", err)
}
defer resp.Body.Close()
file, err := os.Create(filePath)
if err != nil {
return fmt.Errorf("unable to create file: %v", err)
}
defer file.Close()
contentLength := resp.ContentLength
if contentLength <= 0 {
return fmt.Errorf("invalid content length")
}
progressBar.Max = float64(contentLength)
buffer := make([]byte, 4096)
var totalBytes int64
for {
n, readErr := resp.Body.Read(buffer)
if n > 0 {
totalBytes += int64(n)
progressBar.SetValue(float64(totalBytes))
if _, writeErr := file.Write(buffer[:n]); writeErr != nil {
return fmt.Errorf("error writing to file: %v", writeErr)
}
}
if readErr == io.EOF {
break
}
if readErr != nil {
return fmt.Errorf("download aborted: %v", readErr)
}
}
return nil
}