├── .drone.jsonnet ├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── LICENSE ├── README.md ├── authelia ├── authelia.sh ├── package.sh └── test.sh ├── backend ├── access │ ├── external_address.go │ ├── external_address_test.go │ ├── port_probe.go │ ├── port_probe_request.go │ └── port_probe_test.go ├── activation │ ├── device.go │ ├── device_test.go │ ├── domain_custom.go │ ├── domain_custom_test.go │ ├── domain_managed.go │ └── domain_managed_test.go ├── auth │ ├── ldap.go │ ├── ldap_test.go │ ├── secret_generator.go │ ├── secret_generator_test.go │ ├── system_password.go │ ├── web.go │ └── web_test.go ├── backup │ ├── auto.go │ ├── backup.go │ ├── backup_test.go │ ├── file.go │ └── file_test.go ├── cert │ ├── certbot.go │ ├── dns.go │ ├── fake.go │ ├── generator.go │ ├── generator_test.go │ ├── http.go │ └── info.go ├── cli │ ├── executor.go │ ├── executor_test.go │ ├── remover.go │ └── remover_test.go ├── cmd │ ├── api │ │ └── main.go │ ├── backend │ │ └── main.go │ ├── cli │ │ ├── backup.go │ │ ├── btrfs.go │ │ ├── cert.go │ │ ├── config.go │ │ ├── cron.go │ │ ├── ioc.go │ │ ├── ipv4.go │ │ ├── ipv6.go │ │ └── main.go │ ├── install │ │ └── main.go │ └── post-refresh │ │ └── main.go ├── config │ ├── system_config.go │ ├── system_config_test.go │ ├── user_config.go │ └── user_config_test.go ├── connection │ └── internet.go ├── cron │ ├── backup_job.go │ ├── backup_job_test.go │ ├── certificate_job.go │ ├── external_address_job.go │ ├── runner.go │ ├── scheduler.go │ ├── scheduler_test.go │ ├── time_sync_job.go │ └── time_sync_job_test.go ├── date │ ├── provider.go │ └── provider_test.go ├── du │ ├── disk_usage.go │ └── disk_usage_test.go ├── event │ ├── trigger.go │ └── trigger_test.go ├── go.mod ├── go.sum ├── hook │ └── install.go ├── http │ └── client.go ├── identification │ ├── parser.go │ └── parser_test.go ├── installer │ ├── installer.go │ └── model.go ├── ioc │ ├── common.go │ ├── common_test.go │ ├── internal_api.go │ ├── internal_api_test.go │ ├── public_api.go │ ├── public_api_test.go │ └── service.go ├── job │ ├── master.go │ ├── master_test.go │ ├── status.go │ ├── worker.go │ └── worker_test.go ├── log │ ├── category.go │ └── default.go ├── network │ ├── iface.go │ ├── interfaces.go │ └── interfaces_test.go ├── nginx │ ├── service.go │ └── service_test.go ├── parser │ ├── template.go │ ├── template_test.go │ └── test │ │ ├── input │ │ ├── template1.txt │ │ └── template2.txt │ │ └── output │ │ ├── template1.txt │ │ └── template2.txt ├── redirect │ ├── model.go │ ├── passthrough_error.go │ ├── redirect.go │ └── redirect_test.go ├── rest │ ├── activate.go │ ├── activate_test.go │ ├── api.go │ ├── backend.go │ ├── certificate.go │ ├── handler.go │ ├── middleware.go │ ├── middleware_test.go │ ├── model │ │ ├── app_action.go │ │ ├── model.go │ │ ├── parameters_error.go │ │ ├── service_error.go │ │ └── user_login_request.go │ ├── proxy.go │ └── proxy_test.go ├── session │ └── cookies.go ├── snap │ ├── changes_client.go │ ├── changes_client_test.go │ ├── cli.go │ ├── cli_test.go │ ├── client.go │ ├── client_test.go │ ├── model │ │ ├── app.go │ │ ├── change.go │ │ ├── change_test.go │ │ ├── installe_request.go │ │ ├── installer_info.go │ │ ├── installer_status_response.go │ │ ├── server_error.go │ │ ├── server_response.go │ │ ├── snap.go │ │ ├── snap_response.go │ │ ├── snap_test.go │ │ ├── snaps_response.go │ │ ├── syncloud_app.go │ │ ├── syncloud_app_versions.go │ │ └── system_info.go │ ├── server.go │ └── server_test.go ├── storage │ ├── btrfs │ │ ├── device_stats.go │ │ ├── disk.go │ │ ├── disk_test.go │ │ ├── stats.go │ │ └── stats_test.go │ ├── disks.go │ ├── disks_test.go │ ├── free_space_checker.go │ ├── free_space_checker_test.go │ ├── linker.go │ ├── lsblk.go │ ├── lsblk_test.go │ ├── model │ │ ├── disk.go │ │ ├── disk_test.go │ │ ├── lsblk_entry.go │ │ ├── lsblk_entry_test.go │ │ └── partition.go │ ├── path_checker.go │ ├── storage.go │ └── storage_test.go ├── support │ ├── aggregator.go │ ├── aggregator_test.go │ └── sender.go ├── systemd │ ├── control.go │ ├── control_test.go │ ├── journal.go │ ├── journal_test.go │ └── mount.go └── version │ └── meta.go ├── bin ├── boot_extend.sh ├── copy_to_emmc.sh ├── cpu_frequency ├── disk_format.sh ├── service.api.sh ├── service.authelia.sh ├── service.backend.sh ├── service.nginx-public.sh ├── service.openldap.sh ├── update_certs.sh └── upgrade-snapd.sh ├── config ├── .gitignore ├── authelia │ ├── authrequest.conf │ ├── config.yml │ ├── location.conf │ ├── proxy.conf │ └── users.yaml ├── ldap │ ├── init.ldif │ ├── slapd.ldif │ └── upgrade │ │ └── cn=module{0}.ldif ├── mount │ └── mount.template ├── nginx │ └── public.conf └── platform.cfg ├── meta └── snap.yaml ├── nginx ├── bin │ └── nginx.sh ├── build.sh └── test.sh ├── package.sh ├── test ├── .gitignore ├── __init__.py ├── api │ ├── api.go │ ├── api_test.go │ ├── go.mod │ ├── go.sum │ └── model.go ├── conftest.py ├── deps.sh ├── id.cfg ├── install-snapd.sh ├── requirements.txt ├── test-ui.py ├── test-upgrade.py ├── test.desktop.ldif ├── test.mobile.ldif ├── test.py ├── testapp │ ├── bin │ │ ├── access-change │ │ ├── service.sh │ │ └── storage-change │ ├── build.sh │ └── meta │ │ ├── hooks │ │ └── install │ │ └── snap.yaml └── wait-ssh.sh ├── wiki └── images │ ├── gparted.png │ └── performance.png └── www ├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── auto-imports.d.ts ├── babel.config.js ├── components.d.ts ├── index.html ├── jest.config.js ├── package-lock.json ├── package.json ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── images │ ├── penguin.png │ └── wordpress-128.png ├── mstile-150x150.png └── safari-pinned-tab.svg ├── src ├── VueApp.vue ├── assets │ ├── appcenter.svg │ ├── appcenterh.svg │ ├── apps.svg │ ├── appsh.svg │ ├── email.svg │ ├── emailinput.svg │ ├── error.svg │ ├── facebook.svg │ ├── loading.svg │ ├── logo_white.svg │ ├── logout.svg │ ├── logouth.svg │ ├── nameinput.svg │ ├── passinput.svg │ ├── settings.svg │ ├── settingsh.svg │ ├── shutdown.svg │ └── shutdownh.svg ├── components │ ├── Dialog.vue │ ├── Error.vue │ ├── Menu.vue │ └── Notification.vue ├── js │ └── common.js ├── main.js ├── router │ └── index.js ├── stub │ └── api.js ├── style │ └── site.css └── views │ ├── Access.vue │ ├── Activate.vue │ ├── Activation.vue │ ├── App.vue │ ├── AppCenter.vue │ ├── Apps.vue │ ├── Backup.vue │ ├── Certificate.vue │ ├── CertificateLog.vue │ ├── InternalMemory.vue │ ├── Login.vue │ ├── Logs.vue │ ├── Network.vue │ ├── Settings.vue │ ├── Storage.vue │ ├── Support.vue │ └── Updates.vue ├── tests ├── setup-after-env.js ├── setup.js └── unit │ ├── Access.spec.js │ ├── Activate.spec.js │ ├── Activation.spec.js │ ├── App.spec.js │ ├── AppCenter.spec.js │ ├── Apps.spec.js │ ├── Backup.spec.js │ ├── Certificate.spec.js │ ├── CertificateLog.spec.js │ ├── Error.spec.js │ ├── InternalMemory.spec.js │ ├── Login.spec.js │ ├── Logs.spec.js │ ├── Storage.spec.js │ ├── Support.spec.js │ ├── Updates.spec.js │ └── VueApp.spec.js ├── tsconfig.json ├── vite.config.js └── vue.config.js /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Please discuss first at https://syncloud.discourse.group** 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. 14 | 15 | **To Reproduce** 16 | Steps to reproduce the behaviour: 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | **Expected behaviour** 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Screenshots** 26 | If applicable, add screenshots to help explain your problem. 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.pyc 3 | MANIFEST 4 | dist 5 | build 6 | .coverage 7 | *.egg-info 8 | *.tar.gz 9 | rootfs* 10 | _site 11 | 3rdparty 12 | *.iml 13 | .cache 14 | .coin.cache 15 | ./version 16 | logs 17 | *.snap 18 | .pytest_cache 19 | venv -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Syncloud (https://syncloud.org) 2 | 3 | Simple self-hosting of popular apps. 4 | 5 | It is available as an image or a pre-built [device](https://shop.syncloud.org). 6 | 7 | We are open to cooperation with hardware vendors interested in including Syncloud into their products. 8 | 9 | ### Apps 10 | 11 | https://syncloud.org/apps.html 12 | 13 | ### Download 14 | 15 | There are images for various devices and architectures, get one [here](https://github.com/syncloud/platform/wiki). 16 | 17 | ## For developers 18 | 19 | Syncloud image contains the following components: 20 | 21 | 1. Debian based [linux OS](https://github.com/syncloud/image). 22 | 2. Snap based app [installer](https://github.com/syncloud/snapd). 23 | 3. Platform snap package. 24 | 25 | Platform provides shared services for all the apps and manages device settings. 26 | 27 | ### Web UI development 28 | 29 | Install [Node.js](https://nodejs.org/en/download) 30 | 31 | ```` 32 | cd www 33 | npm i 34 | npm run dev 35 | ```` 36 | 37 | ### Building a package locally 38 | We use Drone build server for automated builds. 39 | The simplest way to build a platform snap package locally is to run [drone cli](http://docs.drone.io/cli-installation): 40 | ```` 41 | /path/to/cli/drone jsonnet --stream 42 | sudo /path/to/cli/drone exec --pipeline=[amd64|arm64|arm] --trusted 43 | ```` 44 | 45 | ### Install a package on a device 46 | ```` 47 | snap install --devmode /path/to/package.snap 48 | ```` 49 | -------------------------------------------------------------------------------- /authelia/authelia.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 3 | ${DIR}/lib/ld-musl-*.so* --library-path ${DIR}/lib ${DIR}/authelia "$@" 4 | -------------------------------------------------------------------------------- /authelia/package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ex 2 | 3 | DIR=$( cd "$( dirname "$0" )" && pwd ) 4 | cd ${DIR} 5 | BUILD_DIR=${DIR}/../build/snap/authelia 6 | mkdir -p ${BUILD_DIR} 7 | cp /app/authelia ${BUILD_DIR} 8 | cp -r /lib ${BUILD_DIR} 9 | cp ${DIR}/authelia.sh ${BUILD_DIR} 10 | -------------------------------------------------------------------------------- /authelia/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ex 2 | 3 | DIR=$( cd "$( dirname "$0" )" && pwd ) 4 | cd ${DIR} 5 | BUILD_DIR=${DIR}/../build/snap/authelia 6 | ldd ${BUILD_DIR}/authelia 7 | ${BUILD_DIR}/authelia.sh -v -------------------------------------------------------------------------------- /backend/access/port_probe.go: -------------------------------------------------------------------------------- 1 | package access 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/syncloud/platform/http" 7 | "go.uber.org/zap" 8 | "io" 9 | "net" 10 | ) 11 | 12 | type ProbeUserConfig interface { 13 | GetRedirectApiUrl() string 14 | GetDomainUpdateToken() *string 15 | } 16 | 17 | type PortProbe struct { 18 | userConfig ProbeUserConfig 19 | client http.Client 20 | logger *zap.Logger 21 | } 22 | 23 | func NewProbe(userConfig ProbeUserConfig, client http.Client, logger *zap.Logger) *PortProbe { 24 | return &PortProbe{ 25 | userConfig: userConfig, 26 | client: client, 27 | logger: logger, 28 | } 29 | } 30 | 31 | func (p *PortProbe) Probe(ip string, port int) error { 32 | p.logger.Info(fmt.Sprintf("probing port %v", port)) 33 | 34 | url := fmt.Sprintf("%s/%s", p.userConfig.GetRedirectApiUrl(), "probe/port_v3") 35 | token := p.userConfig.GetDomainUpdateToken() 36 | if token == nil { 37 | return fmt.Errorf("token is not set") 38 | } 39 | 40 | request := &PortProbeRequest{Token: *token, Ip: &ip, Port: port} 41 | 42 | p.logger.Info(fmt.Sprintf("probing ip %v, token: %s", ip, *token)) 43 | 44 | addr := net.ParseIP(ip) 45 | if addr.To4() == nil && addr.To16() == nil { 46 | return fmt.Errorf("IP: %v is not valid", ip) 47 | } 48 | 49 | if addr.IsPrivate() { 50 | return fmt.Errorf("IP: %v is not public", ip) 51 | } 52 | 53 | requestJson, err := json.Marshal(request) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | response, err := p.client.Post(url, "application/json", requestJson) 59 | if err != nil { 60 | return err 61 | } 62 | p.logger.Info(fmt.Sprintf("response status: %v", response.StatusCode)) 63 | defer response.Body.Close() 64 | body, err := io.ReadAll(response.Body) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | p.logger.Info(fmt.Sprintf("response text: %v", string(body))) 70 | 71 | var probeResponse Response 72 | err = json.Unmarshal(body, &probeResponse) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | if !probeResponse.Success { 78 | message := "Unable to verify open ports" 79 | if probeResponse.Message != nil { 80 | message = fmt.Sprintf("%v, %v", message, *probeResponse.Message) 81 | } 82 | return fmt.Errorf(message) 83 | } 84 | 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /backend/access/port_probe_request.go: -------------------------------------------------------------------------------- 1 | package access 2 | 3 | type PortProbeRequest struct { 4 | Token string `json:"token,omitempty"` 5 | Port int `json:"port,omitempty"` 6 | Ip *string `json:"ip,omitempty"` 7 | } 8 | -------------------------------------------------------------------------------- /backend/access/port_probe_test.go: -------------------------------------------------------------------------------- 1 | package access 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/syncloud/platform/log" 12 | ) 13 | 14 | type UserConfigStub struct { 15 | } 16 | 17 | func (u UserConfigStub) GetRedirectApiUrl() string { 18 | return "url" 19 | } 20 | 21 | func (u UserConfigStub) GetDomainUpdateToken() *string { 22 | token := "token" 23 | return &token 24 | } 25 | 26 | type ClientStub struct { 27 | response string 28 | status int 29 | } 30 | 31 | func (c *ClientStub) Get(_ string) (*http.Response, error) { 32 | panic("implement me") 33 | } 34 | 35 | func (c *ClientStub) Post(_, _ string, _ interface{}) (*http.Response, error) { 36 | if c.status != 200 { 37 | return nil, fmt.Errorf("error code: %v", c.status) 38 | } 39 | 40 | r := io.NopCloser(bytes.NewReader([]byte(c.response))) 41 | return &http.Response{ 42 | StatusCode: c.status, 43 | Body: r, 44 | }, nil 45 | } 46 | 47 | func TestProbe_Ok_GoodResponse(t *testing.T) { 48 | client := &ClientStub{`{"success":true,"data":"OK"}`, 200} 49 | probe := NewProbe(&UserConfigStub{}, client, log.Default()) 50 | err := probe.Probe("1.1.1.1", 1) 51 | assert.Nil(t, err) 52 | } 53 | 54 | func TestProbe_Fail_BadResponse(t *testing.T) { 55 | client := &ClientStub{`{"success":false,"message":"error"}`, 200} 56 | probe := NewProbe(&UserConfigStub{}, client, log.Default()) 57 | err := probe.Probe("1.1.1.1", 1) 58 | assert.NotNil(t, err) 59 | assert.Contains(t, err.Error(), "Unable to verify") 60 | } 61 | 62 | func TestProbe_Fail_NotAPublicIp(t *testing.T) { 63 | client := &ClientStub{`{"success":false,"message":"error"}`, 200} 64 | probe := NewProbe(&UserConfigStub{}, client, log.Default()) 65 | err := probe.Probe("192.168.1.1", 1) 66 | assert.NotNil(t, err) 67 | assert.Contains(t, err.Error(), "IP: 192.168.1.1 is not public") 68 | } 69 | 70 | func TestProbe_Fail_NotValidIp(t *testing.T) { 71 | client := &ClientStub{`{"success":false,"message":"error"}`, 200} 72 | probe := NewProbe(&UserConfigStub{}, client, log.Default()) 73 | err := probe.Probe("1.1.1", 1) 74 | assert.NotNil(t, err) 75 | assert.Contains(t, err.Error(), "IP: 1.1.1 is not valid") 76 | } 77 | -------------------------------------------------------------------------------- /backend/activation/device_test.go: -------------------------------------------------------------------------------- 1 | package activation 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestEmail(t *testing.T) { 9 | username, email := ParseUsername("test@example.com", "domain") 10 | assert.Equal(t, "test", username) 11 | assert.Equal(t, "test@example.com", email) 12 | } 13 | 14 | func TestNonEmail(t *testing.T) { 15 | username, email := ParseUsername("test", "domain") 16 | assert.Equal(t, "test", username) 17 | assert.Equal(t, "test@domain", email) 18 | } 19 | -------------------------------------------------------------------------------- /backend/activation/domain_custom.go: -------------------------------------------------------------------------------- 1 | package activation 2 | 3 | import ( 4 | "github.com/syncloud/platform/cert" 5 | "github.com/syncloud/platform/connection" 6 | "go.uber.org/zap" 7 | "strings" 8 | ) 9 | 10 | type CustomActivateRequest struct { 11 | Domain string `json:"domain"` 12 | DeviceUsername string `json:"device_username"` 13 | DevicePassword string `json:"device_password"` 14 | } 15 | 16 | type CustomPlatformUserConfig interface { 17 | SetRedirectEnabled(enabled bool) 18 | SetUserEmail(userEmail string) 19 | SetCustomDomain(domain string) 20 | } 21 | 22 | type CustomActivation interface { 23 | Activate(requestDomain string, deviceUsername string, devicePassword string) error 24 | } 25 | 26 | type Custom struct { 27 | internet connection.InternetChecker 28 | config CustomPlatformUserConfig 29 | device DeviceActivation 30 | cert cert.Generator 31 | logger *zap.Logger 32 | } 33 | 34 | func NewCustom(internet connection.InternetChecker, config CustomPlatformUserConfig, device DeviceActivation, 35 | cert cert.Generator, logger *zap.Logger) *Custom { 36 | return &Custom{ 37 | internet: internet, 38 | config: config, 39 | device: device, 40 | cert: cert, 41 | logger: logger, 42 | } 43 | } 44 | 45 | func (c *Custom) Activate(requestDomain string, deviceUsername string, devicePassword string) error { 46 | c.logger.Info("activate custom", zap.String("requestDomain", requestDomain), zap.String("deviceUsername", deviceUsername)) 47 | domain := strings.ToLower(requestDomain) 48 | 49 | err := c.internet.Check() 50 | if err != nil { 51 | return err 52 | } 53 | 54 | c.config.SetRedirectEnabled(false) 55 | c.config.SetCustomDomain(domain) 56 | name, email := ParseUsername(deviceUsername, domain) 57 | c.config.SetUserEmail(email) 58 | 59 | err = c.cert.Generate() 60 | if err != nil { 61 | return err 62 | } 63 | 64 | return c.device.ActivateDevice(deviceUsername, devicePassword, name, email) 65 | } 66 | -------------------------------------------------------------------------------- /backend/activation/domain_custom_test.go: -------------------------------------------------------------------------------- 1 | package activation 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "github.com/syncloud/platform/log" 7 | "testing" 8 | ) 9 | 10 | type CustomPlatformUserConfigStub struct { 11 | domain string 12 | } 13 | 14 | func (c *CustomPlatformUserConfigStub) SetRedirectEnabled(enabled bool) { 15 | } 16 | 17 | func (c *CustomPlatformUserConfigStub) SetUserEmail(userEmail string) { 18 | 19 | } 20 | 21 | func (c *CustomPlatformUserConfigStub) SetCustomDomain(domain string) { 22 | c.domain = domain 23 | } 24 | 25 | type CustorCertbotStub struct { 26 | attempted int 27 | generated int 28 | fail bool 29 | } 30 | 31 | func (c *CustorCertbotStub) Generate() error { 32 | c.attempted += 1 33 | if c.fail { 34 | return fmt.Errorf("error") 35 | } 36 | c.generated++ 37 | return nil 38 | } 39 | 40 | func TestManaged_ActivateCustom_GenerateFakeCertificate(t *testing.T) { 41 | logger := log.Default() 42 | 43 | cert := &CustorCertbotStub{} 44 | config := &CustomPlatformUserConfigStub{} 45 | managed := NewCustom(&InternetCheckerStub{}, config, &DeviceActivationStub{}, cert, logger) 46 | err := managed.Activate("example.com", "username", "password") 47 | assert.Nil(t, err) 48 | 49 | assert.Equal(t, 1, cert.generated) 50 | } 51 | 52 | func TestManaged_ActivateCustom_FixDomainName(t *testing.T) { 53 | logger := log.Default() 54 | 55 | cert := &CustorCertbotStub{} 56 | config := &CustomPlatformUserConfigStub{} 57 | managed := NewCustom(&InternetCheckerStub{}, config, &DeviceActivationStub{}, cert, logger) 58 | err := managed.Activate("ExaMple.com", "username", "password") 59 | assert.Nil(t, err) 60 | 61 | assert.Equal(t, "example.com", config.domain) 62 | assert.Equal(t, 1, cert.generated) 63 | } 64 | -------------------------------------------------------------------------------- /backend/auth/ldap_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "fmt" 5 | "github.com/syncloud/platform/log" 6 | "os" 7 | "path" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | type SnapServiceStub struct { 15 | } 16 | 17 | func (s SnapServiceStub) Stop(_ string) error { 18 | return nil 19 | } 20 | 21 | func (s SnapServiceStub) Start(_ string) error { 22 | return nil 23 | } 24 | 25 | type ExecutorStub struct { 26 | executions []string 27 | } 28 | 29 | func (e *ExecutorStub) CombinedOutput(name string, arg ...string) ([]byte, error) { 30 | e.executions = append(e.executions, fmt.Sprintf("%s %s", name, strings.Join(arg, " "))) 31 | return []byte(""), nil 32 | } 33 | 34 | type PasswordChangerStub struct { 35 | changed bool 36 | } 37 | 38 | func (p *PasswordChangerStub) Change(_ string) error { 39 | p.changed = true 40 | return nil 41 | } 42 | 43 | func TestMakeSecret(t *testing.T) { 44 | secret := makeSecret("syncloud") 45 | assert.Greater(t, len(secret), 1) 46 | } 47 | 48 | func TestInit(t *testing.T) { 49 | executor := &ExecutorStub{} 50 | ldap := New(&SnapServiceStub{}, t.TempDir(), t.TempDir(), t.TempDir(), executor, &PasswordChangerStub{}, log.Default()) 51 | err := ldap.Init() 52 | assert.Nil(t, err) 53 | assert.Len(t, executor.executions, 1) 54 | assert.Contains(t, executor.executions[0], "slapadd.sh") 55 | } 56 | 57 | func TestReset(t *testing.T) { 58 | executor := &ExecutorStub{} 59 | configDir := t.TempDir() 60 | err := os.MkdirAll(path.Join(configDir, "ldap"), os.ModePerm) 61 | assert.Nil(t, err) 62 | err = os.WriteFile(path.Join(configDir, "ldap", "init.ldif"), []byte("template"), 0644) 63 | assert.Nil(t, err) 64 | 65 | passwordChanger := &PasswordChangerStub{} 66 | ldap := New(&SnapServiceStub{}, t.TempDir(), t.TempDir(), configDir, executor, passwordChanger, log.Default()) 67 | err = ldap.Reset("name", "user", "password", "email") 68 | assert.Nil(t, err) 69 | assert.Len(t, executor.executions, 2) 70 | assert.Contains(t, executor.executions[0], "slapadd.sh") 71 | assert.Contains(t, executor.executions[1], "ldapadd.sh") 72 | assert.True(t, passwordChanger.changed) 73 | 74 | } 75 | -------------------------------------------------------------------------------- /backend/auth/secret_generator.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "fmt" 5 | "github.com/syncloud/platform/cli" 6 | "strings" 7 | ) 8 | 9 | type SecretGenerator struct { 10 | executor cli.Executor 11 | } 12 | 13 | type Secret struct { 14 | Password string 15 | Hash string 16 | } 17 | 18 | func NewSecretGenerator(executor cli.Executor) *SecretGenerator { 19 | return &SecretGenerator{ 20 | executor: executor, 21 | } 22 | } 23 | 24 | func (s *SecretGenerator) Generate() (Secret, error) { 25 | output, err := s.executor.CombinedOutput("snap", "run", "platform.authelia-cli", "crypto", "hash", "generate", "--random") 26 | if err != nil { 27 | return Secret{}, err 28 | } 29 | 30 | parts := strings.Split(string(output), "\n") 31 | if len(parts) < 2 { 32 | return Secret{}, fmt.Errorf("not valid authelia crypto response: %s", string(output)) 33 | } 34 | password, found := strings.CutPrefix(parts[0], "Random Password: ") 35 | if !found { 36 | return Secret{}, fmt.Errorf("not valid authelia crypto password: %s", parts[0]) 37 | } 38 | hash, found := strings.CutPrefix(parts[1], "Digest: ") 39 | if !found { 40 | return Secret{}, fmt.Errorf("not valid authelia crypto hash: %s", parts[1]) 41 | } 42 | return Secret{ 43 | Password: password, 44 | Hash: hash, 45 | }, err 46 | } 47 | -------------------------------------------------------------------------------- /backend/auth/secret_generator_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | type GeneratorExecutorStub struct { 9 | output string 10 | } 11 | 12 | func (e *GeneratorExecutorStub) CombinedOutput(name string, arg ...string) ([]byte, error) { 13 | return []byte(e.output), nil 14 | } 15 | 16 | func TestSecretGenerator_Generate(t *testing.T) { 17 | 18 | generator := &SecretGenerator{executor: &GeneratorExecutorStub{ 19 | `Random Password: Dtf0qf8eoVJBaSCPU7hbYFUAOKbBahP5Pgf9VTDssA17dfCG1ilFph6PrBljr1aQFhCfy3TW 20 | Digest: $argon2id$v=19$m=65536,t=3,p=4$7TGF3l00V1y6pJQPqmalnQ$qziJ0fKC23V/tpGhze97RJ9TbVbKae3CyZCcBiqmI5I 21 | `}} 22 | secret, err := generator.Generate() 23 | assert.NoError(t, err) 24 | 25 | assert.Equal(t, "$argon2id$v=19$m=65536,t=3,p=4$7TGF3l00V1y6pJQPqmalnQ$qziJ0fKC23V/tpGhze97RJ9TbVbKae3CyZCcBiqmI5I", secret.Hash) 26 | assert.Equal(t, "Dtf0qf8eoVJBaSCPU7hbYFUAOKbBahP5Pgf9VTDssA17dfCG1ilFph6PrBljr1aQFhCfy3TW", secret.Password) 27 | } 28 | -------------------------------------------------------------------------------- /backend/auth/system_password.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "fmt" 5 | "go.uber.org/zap" 6 | "io" 7 | "os/exec" 8 | ) 9 | 10 | type SystemPasswordChanger struct { 11 | logger *zap.Logger 12 | } 13 | 14 | type PasswordChanger interface { 15 | Change(password string) error 16 | } 17 | 18 | func NewSystemPassword(logger *zap.Logger) *SystemPasswordChanger { 19 | return &SystemPasswordChanger{logger: logger} 20 | } 21 | 22 | func (s *SystemPasswordChanger) Change(password string) error { 23 | cmd := exec.Command("chpasswd") 24 | stdin, err := cmd.StdinPipe() 25 | if err != nil { 26 | return err 27 | } 28 | go func() { 29 | defer func() { _ = stdin.Close() }() 30 | io.WriteString(stdin, fmt.Sprintf("root:%s\n", password)) 31 | }() 32 | out, err := cmd.CombinedOutput() 33 | s.logger.Info("chpasswd", zap.ByteString("output", out)) 34 | return err 35 | } 36 | -------------------------------------------------------------------------------- /backend/backup/auto.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | type Auto struct { 4 | Auto string `json:"auto"` 5 | Day int `json:"day"` 6 | Hour int `json:"hour"` 7 | } 8 | -------------------------------------------------------------------------------- /backend/backup/file.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | ) 7 | 8 | type File struct { 9 | Path string `json:"path"` 10 | File string `json:"file"` 11 | App string `json:"app"` 12 | FullName string `json:"-"` 13 | } 14 | 15 | func Parse(path string, fileName string) (File, error) { 16 | r, err := regexp.Compile(`(.*?)-\d{4}-\d{4}.*`) 17 | if err != nil { 18 | return File{}, err 19 | } 20 | 21 | matches := r.FindStringSubmatch(fileName) 22 | if len(matches) < 2 { 23 | return File{}, fmt.Errorf("backup file name should start with '[app]-YYYY-MMDD-'") 24 | } 25 | app := matches[1] 26 | return File{ 27 | Path: path, 28 | File: fileName, 29 | App: app, 30 | FullName: fmt.Sprintf("%s/%s", path, fileName), 31 | }, nil 32 | } 33 | -------------------------------------------------------------------------------- /backend/backup/file_test.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestParse_Simple(t *testing.T) { 9 | file, err := Parse("/data", "app-2001-020304-050607.tar.gz") 10 | assert.Nil(t, err) 11 | assert.Equal(t, file.Path, "/data") 12 | assert.Equal(t, file.File, "app-2001-020304-050607.tar.gz") 13 | assert.Equal(t, file.App, "app") 14 | assert.Equal(t, file.FullName, "/data/app-2001-020304-050607.tar.gz") 15 | } 16 | 17 | func TestParse_AppWithDash(t *testing.T) { 18 | file, err := Parse("/data", "app-name-2001-020304-050607.tar.gz") 19 | assert.Nil(t, err) 20 | assert.Equal(t, file.Path, "/data") 21 | assert.Equal(t, file.File, "app-name-2001-020304-050607.tar.gz") 22 | assert.Equal(t, file.App, "app-name") 23 | assert.Equal(t, file.FullName, "/data/app-name-2001-020304-050607.tar.gz") 24 | } 25 | 26 | func TestParse_Wrong(t *testing.T) { 27 | _, err := Parse("/data", "app-name-2001_020304-050607.tar.gz") 28 | assert.NotNil(t, err) 29 | } 30 | -------------------------------------------------------------------------------- /backend/cert/dns.go: -------------------------------------------------------------------------------- 1 | package cert 2 | 3 | import ( 4 | "fmt" 5 | "github.com/go-acme/lego/v4/challenge/dns01" 6 | "go.uber.org/zap" 7 | "time" 8 | ) 9 | 10 | type SyncloudDNS struct { 11 | token string 12 | redirect RedirectCertbot 13 | values []string 14 | certbotLogger *zap.Logger 15 | } 16 | 17 | type RedirectCertbot interface { 18 | CertbotPresent(token, fqdn string, value ...string) error 19 | CertbotCleanUp(token, fqdn string) error 20 | } 21 | 22 | func NewSyncloudDNS(token string, redirect RedirectCertbot, certbotLogger *zap.Logger) *SyncloudDNS { 23 | return &SyncloudDNS{ 24 | token: token, 25 | redirect: redirect, 26 | certbotLogger: certbotLogger, 27 | } 28 | } 29 | 30 | func (d *SyncloudDNS) Present(domain, _, keyAuth string) error { 31 | info := dns01.GetChallengeInfo(domain, keyAuth) 32 | d.values = append(d.values, info.Value) 33 | err := d.redirect.CertbotPresent(d.token, info.EffectiveFQDN, d.values...) 34 | if err != nil { 35 | d.certbotLogger.Error(fmt.Sprintf("dns present error: %s", err.Error())) 36 | } 37 | return err 38 | } 39 | 40 | func (d *SyncloudDNS) CleanUp(domain, _, keyAuth string) error { 41 | d.values = make([]string, 0) 42 | info := dns01.GetChallengeInfo(domain, keyAuth) 43 | err := d.redirect.CertbotCleanUp(d.token, info.EffectiveFQDN) 44 | if err != nil { 45 | d.certbotLogger.Error(fmt.Sprintf("dns cleanup error: %s", err.Error())) 46 | } 47 | return err 48 | } 49 | 50 | func (d *SyncloudDNS) Timeout() (timeout, interval time.Duration) { 51 | return 5 * time.Minute, 5 * time.Second 52 | } 53 | -------------------------------------------------------------------------------- /backend/cert/http.go: -------------------------------------------------------------------------------- 1 | package cert 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | type HttpProviderSyncloud struct{} 8 | 9 | const Path = "/var/snap/platform/current/certbot/www/.well-known/acme-challenge/" 10 | 11 | func NewHttpProviderSyncloud() *HttpProviderSyncloud { 12 | return &HttpProviderSyncloud{} 13 | } 14 | 15 | func (d *HttpProviderSyncloud) Present(_, token, keyAuth string) error { 16 | path := Path + token 17 | err := os.WriteFile(path, []byte(keyAuth), 0644) 18 | return err 19 | } 20 | 21 | func (d *HttpProviderSyncloud) CleanUp(_, token, keyAuth string) error { 22 | return os.Remove(Path + token) 23 | } 24 | -------------------------------------------------------------------------------- /backend/cert/info.go: -------------------------------------------------------------------------------- 1 | package cert 2 | 3 | type Info struct { 4 | Subject string `json:"-"` 5 | IsValid bool `json:"is_valid"` 6 | IsReal bool `json:"is_real"` 7 | ValidForDays int `json:"valid_for_days"` 8 | } 9 | -------------------------------------------------------------------------------- /backend/cli/executor.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "go.uber.org/zap" 6 | "os/exec" 7 | ) 8 | 9 | type ShellExecutor struct { 10 | logger *zap.Logger 11 | } 12 | 13 | type Executor interface { 14 | CombinedOutput(name string, arg ...string) ([]byte, error) 15 | } 16 | 17 | func New(logger *zap.Logger) *ShellExecutor { 18 | return &ShellExecutor{logger: logger} 19 | } 20 | 21 | func (e *ShellExecutor) CombinedOutput(name string, arg ...string) ([]byte, error) { 22 | command := exec.Command(name, arg...) 23 | e.logger.Info("execute", zap.String("cmd", command.String())) 24 | output, err := command.CombinedOutput() 25 | if err != nil { 26 | return output, fmt.Errorf("%v: %s", err, string(output)) 27 | } 28 | return output, err 29 | } 30 | -------------------------------------------------------------------------------- /backend/cli/executor_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "github.com/syncloud/platform/log" 6 | "testing" 7 | ) 8 | 9 | func TestExecutor_CommandOutput(t *testing.T) { 10 | executor := New(log.Default()) 11 | output, err := executor.CombinedOutput("date") 12 | assert.Nil(t, err) 13 | assert.Greater(t, len(string(output)), 0) 14 | } 15 | -------------------------------------------------------------------------------- /backend/cli/remover.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | func Remove(pattern string) error { 9 | files, err := filepath.Glob(pattern) 10 | if err != nil { 11 | return err 12 | } 13 | for _, f := range files { 14 | if err := os.Remove(f); err != nil { 15 | return err 16 | } 17 | } 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /backend/cli/remover_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "os" 7 | "path" 8 | "testing" 9 | ) 10 | 11 | func TestRemove(t *testing.T) { 12 | dir := t.TempDir() 13 | _ = os.WriteFile(path.Join(dir, "1.log"), []byte(""), 644) 14 | _ = os.WriteFile(path.Join(dir, "2.txt"), []byte(""), 644) 15 | _ = os.WriteFile(path.Join(dir, "3.log"), []byte(""), 644) 16 | 17 | err := Remove(fmt.Sprintf("%s/*.log", dir)) 18 | assert.NoError(t, err) 19 | 20 | entries, err := os.ReadDir(dir) 21 | assert.NoError(t, err) 22 | assert.Equal(t, 1, len(entries)) 23 | assert.Equal(t, "2.txt", entries[0].Name()) 24 | 25 | } 26 | -------------------------------------------------------------------------------- /backend/cmd/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/syncloud/platform/backup" 6 | "github.com/syncloud/platform/config" 7 | "github.com/syncloud/platform/ioc" 8 | "os" 9 | ) 10 | 11 | func main() { 12 | 13 | var rootCmd = &cobra.Command{Use: "api"} 14 | configDb := rootCmd.PersistentFlags().String("config", config.DefaultConfigDb, "sqlite config db") 15 | 16 | var unixSocketCmd = &cobra.Command{ 17 | Use: "unix [address]", 18 | Args: cobra.ExactArgs(1), 19 | Short: "listen on a unix socket, like /tmp/api.sock", 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | _ = os.Remove(args[0]) 22 | c, err := ioc.InitInternalApi(*configDb, config.DefaultSystemConfig, backup.Dir, backup.VarDir, "unix", args[0]) 23 | if err != nil { 24 | return err 25 | } 26 | return ioc.Start(c) 27 | }, 28 | } 29 | 30 | rootCmd.AddCommand(unixSocketCmd) 31 | 32 | if err := rootCmd.Execute(); err != nil { 33 | panic(err) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/cmd/backend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/syncloud/platform/backup" 6 | "github.com/syncloud/platform/config" 7 | "github.com/syncloud/platform/ioc" 8 | "os" 9 | ) 10 | 11 | func main() { 12 | 13 | var rootCmd = &cobra.Command{Use: "backend"} 14 | userConfig := rootCmd.PersistentFlags().String("user-config", config.DefaultConfigDb, "sqlite config db") 15 | systemConfig := rootCmd.PersistentFlags().String("system-config", config.DefaultSystemConfig, "system config") 16 | 17 | var tcpCmd = &cobra.Command{ 18 | Use: "tcp [address]", 19 | Short: "listen on a tcp address, like localhost:8080", 20 | Args: cobra.ExactArgs(1), 21 | RunE: func(cmd *cobra.Command, args []string) error { 22 | c, err := ioc.InitPublicApi(*userConfig, *systemConfig, backup.Dir, backup.VarDir, "tcp", args[0]) 23 | if err != nil { 24 | return err 25 | } 26 | return ioc.Start(c) 27 | }, 28 | } 29 | 30 | var unixSocketCmd = &cobra.Command{ 31 | Use: "unix [address]", 32 | Args: cobra.ExactArgs(1), 33 | Short: "listen on a unix socket, like /tmp/backend.sock", 34 | RunE: func(cmd *cobra.Command, args []string) error { 35 | _ = os.Remove(args[0]) 36 | c, err := ioc.InitPublicApi(*userConfig, *systemConfig, backup.Dir, backup.VarDir, "unix", args[0]) 37 | if err != nil { 38 | return err 39 | } 40 | return ioc.Start(c) 41 | }, 42 | } 43 | 44 | rootCmd.AddCommand(tcpCmd, unixSocketCmd) 45 | 46 | if err := rootCmd.Execute(); err != nil { 47 | panic(err) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /backend/cmd/cli/backup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/spf13/cobra" 7 | "github.com/syncloud/platform/backup" 8 | ) 9 | 10 | func backupCmd(userConfig *string, systemConfig *string) *cobra.Command { 11 | var cmd = &cobra.Command{ 12 | Use: "backup", 13 | Short: "Backup create/restore", 14 | } 15 | 16 | cmd.AddCommand(&cobra.Command{ 17 | Use: "create [app]", 18 | Short: "Backup", 19 | Args: cobra.ExactArgs(1), 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | c, err := Init(*userConfig, *systemConfig) 22 | if err != nil { 23 | return err 24 | } 25 | return c.Call(func(backup *backup.Backup) error { 26 | app := args[0] 27 | return backup.Create(app) 28 | }) 29 | }, 30 | }) 31 | 32 | cmd.AddCommand(&cobra.Command{ 33 | Use: "restore [file]", 34 | Short: "Restore file", 35 | Args: cobra.ExactArgs(1), 36 | RunE: func(cmd *cobra.Command, args []string) error { 37 | c, err := Init(*userConfig, *systemConfig) 38 | if err != nil { 39 | return err 40 | } 41 | return c.Call(func(backup *backup.Backup) error { 42 | return backup.Restore(args[0]) 43 | }) 44 | }, 45 | }) 46 | 47 | cmd.AddCommand(&cobra.Command{ 48 | Use: "list", 49 | Short: "List backups", 50 | RunE: func(cmd *cobra.Command, args []string) error { 51 | c, err := Init(*userConfig, *systemConfig) 52 | if err != nil { 53 | return err 54 | } 55 | return c.Call(func(backup *backup.Backup) error { 56 | list, err := backup.List() 57 | if err != nil { 58 | return err 59 | } 60 | s, err := json.MarshalIndent(list, "", "\t") 61 | if err != nil { 62 | return err 63 | } 64 | fmt.Printf("%s\n", s) 65 | return nil 66 | }) 67 | }, 68 | }) 69 | return cmd 70 | } 71 | -------------------------------------------------------------------------------- /backend/cmd/cli/btrfs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/spf13/cobra" 7 | "github.com/syncloud/platform/storage/btrfs" 8 | ) 9 | 10 | func btrfsCmd(userConfig *string, systemConfig *string) *cobra.Command { 11 | var cmdBtrfs = &cobra.Command{ 12 | Use: "btrfs", 13 | Short: "Show btrfs", 14 | RunE: func(cmd *cobra.Command, args []string) error { 15 | c, err := Init(*userConfig, *systemConfig) 16 | if err != nil { 17 | return err 18 | } 19 | return c.Call(func(btrfs *btrfs.Stats) error { 20 | info, err := btrfs.Info() 21 | if err != nil { 22 | return err 23 | } 24 | s, err := json.MarshalIndent(info, "", "\t") 25 | if err != nil { 26 | return err 27 | } 28 | fmt.Printf("btrfs info\n") 29 | fmt.Printf("%s\n", s) 30 | return nil 31 | }) 32 | }, 33 | } 34 | return cmdBtrfs 35 | } 36 | -------------------------------------------------------------------------------- /backend/cmd/cli/cert.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/syncloud/platform/cert" 6 | ) 7 | 8 | func certCmd(userConfig *string, systemConfig *string) *cobra.Command { 9 | var cmdCert = &cobra.Command{ 10 | Use: "cert", 11 | Short: "Generate certificate", 12 | RunE: func(cmd *cobra.Command, args []string) error { 13 | c, err := Init(*userConfig, *systemConfig) 14 | if err != nil { 15 | return err 16 | } 17 | return c.Call(func(certGenerator *cert.CertificateGenerator) error { 18 | return certGenerator.Generate() 19 | }) 20 | }, 21 | } 22 | return cmdCert 23 | } 24 | -------------------------------------------------------------------------------- /backend/cmd/cli/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/cobra" 6 | "github.com/syncloud/platform/config" 7 | ) 8 | 9 | func configCmd(userConfig *string, systemConfig *string) *cobra.Command { 10 | var configFile string 11 | var cmdConfig = &cobra.Command{ 12 | Use: "config", 13 | Short: "Manage config", 14 | } 15 | cmdConfig.PersistentFlags().StringVar(&configFile, "file", config.DefaultConfigDb, "config file") 16 | 17 | var cmdConfigSet = &cobra.Command{ 18 | Use: "set [key] [value]", 19 | Short: "Set config key value", 20 | Args: cobra.ExactArgs(2), 21 | RunE: func(cmd *cobra.Command, args []string) error { 22 | c, err := Init(*userConfig, *systemConfig) 23 | if err != nil { 24 | return err 25 | } 26 | return c.Call(func(configuration *config.UserConfig) { 27 | key := args[0] 28 | value := args[1] 29 | configuration.Upsert(key, value) 30 | fmt.Printf("set config: %s, key: %s, value: %s\n", configFile, key, value) 31 | }) 32 | }, 33 | } 34 | cmdConfig.AddCommand(cmdConfigSet) 35 | 36 | var cmdConfigGet = &cobra.Command{ 37 | Use: "get [key]", 38 | Short: "Get config key value", 39 | Args: cobra.ExactArgs(1), 40 | RunE: func(cmd *cobra.Command, args []string) error { 41 | c, err := Init(*userConfig, *systemConfig) 42 | if err != nil { 43 | return err 44 | } 45 | return c.Call(func(configuration *config.UserConfig) { 46 | fmt.Println(configuration.Get(args[0], "")) 47 | }) 48 | }, 49 | } 50 | cmdConfig.AddCommand(cmdConfigGet) 51 | 52 | var cmdConfigList = &cobra.Command{ 53 | Use: "list", 54 | Short: "List config key value", 55 | Args: cobra.ExactArgs(0), 56 | RunE: func(cmd *cobra.Command, args []string) error { 57 | c, err := Init(*userConfig, *systemConfig) 58 | if err != nil { 59 | return err 60 | } 61 | return c.Call(func(configuration *config.UserConfig) { 62 | for key, value := range configuration.List() { 63 | fmt.Printf("%s:%s\n", key, value) 64 | } 65 | }) 66 | }, 67 | } 68 | cmdConfig.AddCommand(cmdConfigList) 69 | return cmdConfig 70 | } 71 | -------------------------------------------------------------------------------- /backend/cmd/cli/cron.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/syncloud/platform/cron" 6 | ) 7 | 8 | func cronCmd(userConfig *string, systemConfig *string) *cobra.Command { 9 | var cmdCron = &cobra.Command{ 10 | Use: "cron", 11 | Short: "Run cron job", 12 | RunE: func(cmd *cobra.Command, args []string) error { 13 | c, err := Init(*userConfig, *systemConfig) 14 | if err != nil { 15 | return err 16 | } 17 | return c.Call(func(cronService *cron.Cron) { cronService.StartSingle() }) 18 | }, 19 | } 20 | return cmdCron 21 | } 22 | -------------------------------------------------------------------------------- /backend/cmd/cli/ioc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/golobby/container/v3" 5 | "github.com/syncloud/platform/backup" 6 | "github.com/syncloud/platform/ioc" 7 | ) 8 | 9 | func Init(userConfig string, systemConfig string) (container.Container, error) { 10 | return ioc.Init(userConfig, systemConfig, backup.Dir, backup.VarDir) 11 | } 12 | -------------------------------------------------------------------------------- /backend/cmd/cli/ipv4.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/cobra" 6 | "github.com/syncloud/platform/network" 7 | ) 8 | 9 | func ipv4Cmd(userConfig *string, systemConfig *string) *cobra.Command { 10 | var cmdIpv4 = &cobra.Command{ 11 | Use: "ipv4 [public]", 12 | Short: "Print IPv4", 13 | Args: cobra.MaximumNArgs(1), 14 | RunE: func(cmd *cobra.Command, args []string) error { 15 | c, err := Init(*userConfig, *systemConfig) 16 | if err != nil { 17 | panic(err) 18 | } 19 | return c.Call(func(iface *network.TcpInterfaces) error { 20 | ip, err := iface.LocalIPv4() 21 | if err != nil { 22 | return err 23 | } 24 | fmt.Print(ip.String()) 25 | return nil 26 | }) 27 | }, 28 | } 29 | 30 | cmdIpv4.AddCommand(&cobra.Command{ 31 | Use: "public", 32 | Short: "Print public IPv4", 33 | Args: cobra.MaximumNArgs(1), 34 | RunE: func(cmd *cobra.Command, args []string) error { 35 | c, err := Init(*userConfig, *systemConfig) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return c.Call(func(iface *network.TcpInterfaces) error { 40 | ip, err := iface.PublicIPv4() 41 | if err != nil { 42 | return err 43 | } 44 | fmt.Print(*ip) 45 | return nil 46 | }) 47 | }, 48 | }) 49 | return cmdIpv4 50 | } 51 | -------------------------------------------------------------------------------- /backend/cmd/cli/ipv6.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/cobra" 6 | "github.com/syncloud/platform/network" 7 | "net" 8 | "os" 9 | ) 10 | 11 | func ipv6Cmd(userConfig *string, systemConfig *string) *cobra.Command { 12 | var cmdIpv6 = &cobra.Command{ 13 | Use: "ipv6 [prefix]", 14 | Short: "Print IPv6", 15 | Args: cobra.MaximumNArgs(1), 16 | RunE: func(cmd *cobra.Command, args []string) error { 17 | c, err := Init(*userConfig, *systemConfig) 18 | if err != nil { 19 | panic(err) 20 | } 21 | return c.Call(func(iface *network.TcpInterfaces) { 22 | ip, err := iface.IPv6Addr() 23 | if err != nil { 24 | fmt.Print(err) 25 | os.Exit(1) 26 | } 27 | fmt.Print(ip.String()) 28 | }) 29 | }, 30 | } 31 | var prefixSize int 32 | var cmdIpv6prefix = &cobra.Command{ 33 | Use: "prefix", 34 | Short: "Print IPv6 prefix", 35 | RunE: func(cmd *cobra.Command, args []string) error { 36 | c, err := Init(*userConfig, *systemConfig) 37 | if err != nil { 38 | panic(err) 39 | } 40 | return c.Call(func(iface *network.TcpInterfaces) { 41 | ip, err := iface.IPv6Addr() 42 | if err != nil { 43 | fmt.Print(err) 44 | os.Exit(1) 45 | } 46 | fmt.Printf("%v/%v", ip.Mask(net.CIDRMask(prefixSize, 128)), prefixSize) 47 | }) 48 | }, 49 | } 50 | cmdIpv6prefix.Flags().IntVarP(&prefixSize, "size", "s", 64, "Prefix size") 51 | cmdIpv6.AddCommand(cmdIpv6prefix) 52 | return cmdIpv6 53 | } 54 | -------------------------------------------------------------------------------- /backend/cmd/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/cobra" 6 | "github.com/syncloud/platform/config" 7 | "os" 8 | ) 9 | 10 | func main() { 11 | var rootCmd = &cobra.Command{Use: "cli"} 12 | userConfig := rootCmd.PersistentFlags().String("user-config", config.DefaultConfigDb, "user config sqlite db") 13 | systemConfig := rootCmd.PersistentFlags().String("system-config", config.DefaultSystemConfig, "system config file") 14 | 15 | rootCmd.AddCommand( 16 | ipv4Cmd(userConfig, systemConfig), 17 | ipv6Cmd(userConfig, systemConfig), 18 | configCmd(userConfig, systemConfig), 19 | cronCmd(userConfig, systemConfig), 20 | certCmd(userConfig, systemConfig), 21 | btrfsCmd(userConfig, systemConfig), 22 | backupCmd(userConfig, systemConfig), 23 | ) 24 | 25 | err := rootCmd.Execute() 26 | if err != nil { 27 | fmt.Print(err) 28 | os.Exit(1) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/cmd/install/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/cobra" 6 | "github.com/syncloud/platform/backup" 7 | "github.com/syncloud/platform/config" 8 | "github.com/syncloud/platform/hook" 9 | "github.com/syncloud/platform/ioc" 10 | "os" 11 | ) 12 | 13 | func main() { 14 | var rootCmd = &cobra.Command{ 15 | Use: "install", 16 | SilenceUsage: true, 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | c, err := ioc.Init(config.DefaultConfigDb, config.DefaultSystemConfig, backup.Dir, backup.VarDir) 19 | if err != nil { 20 | return err 21 | } 22 | return c.Call(func(install *hook.Install) error { 23 | return install.Install() 24 | }) 25 | }, 26 | } 27 | 28 | err := rootCmd.Execute() 29 | if err != nil { 30 | fmt.Print(err) 31 | os.Exit(1) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backend/cmd/post-refresh/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/cobra" 6 | "github.com/syncloud/platform/backup" 7 | "github.com/syncloud/platform/config" 8 | "github.com/syncloud/platform/hook" 9 | "github.com/syncloud/platform/ioc" 10 | "os" 11 | ) 12 | 13 | func main() { 14 | var rootCmd = &cobra.Command{ 15 | Use: "post-refresh", 16 | SilenceUsage: true, 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | c, err := ioc.Init(config.DefaultConfigDb, config.DefaultSystemConfig, backup.Dir, backup.VarDir) 19 | if err != nil { 20 | return err 21 | } 22 | return c.Call(func(install *hook.Install) error { 23 | return install.PostRefresh() 24 | }) 25 | }, 26 | } 27 | 28 | err := rootCmd.Execute() 29 | if err != nil { 30 | fmt.Print(err) 31 | os.Exit(1) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backend/config/system_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/bigkevmcd/go-configparser" 5 | "log" 6 | ) 7 | 8 | const WebAccessPort = 443 9 | const WebProtocol = "https" 10 | 11 | type SystemConfig struct { 12 | file string 13 | parser *configparser.ConfigParser 14 | } 15 | 16 | var DefaultSystemConfig string 17 | 18 | func init() { 19 | DefaultSystemConfig = "/snap/platform/current/config/platform.cfg" 20 | } 21 | 22 | func NewSystemConfig(file string) *SystemConfig { 23 | return &SystemConfig{ 24 | file: file, 25 | } 26 | } 27 | 28 | func (c *SystemConfig) Load() { 29 | parser, err := configparser.NewConfigParserFromFile(c.file) 30 | if err != nil { 31 | log.Fatalln(err) 32 | } 33 | c.parser = parser 34 | } 35 | 36 | func (c *SystemConfig) DataDir() string { 37 | return c.get("data_dir") 38 | } 39 | 40 | func (c *SystemConfig) DiskRoot() string { 41 | return c.get("disk_root") 42 | } 43 | 44 | func (c *SystemConfig) CommonDir() string { 45 | return c.get("common_dir") 46 | } 47 | 48 | func (c *SystemConfig) AppDir() string { 49 | return c.get("app_dir") 50 | } 51 | 52 | func (c *SystemConfig) ConfigDir() string { 53 | return c.get("config_dir") 54 | } 55 | 56 | func (c *SystemConfig) SslCertificateFile() string { 57 | return c.get("ssl_certificate_file") 58 | } 59 | 60 | func (c *SystemConfig) SslKeyFile() string { 61 | return c.get("ssl_key_file") 62 | } 63 | 64 | func (c *SystemConfig) SslCaCertificateFile() string { 65 | return c.get("ssl_ca_certificate_file") 66 | } 67 | 68 | func (c *SystemConfig) SslCaKeyFile() string { 69 | return c.get("ssl_ca_key_file") 70 | } 71 | 72 | func (c *SystemConfig) Channel() string { 73 | return c.get("channel") 74 | } 75 | 76 | func (c *SystemConfig) DiskLink() string { 77 | return c.get("disk_link") 78 | } 79 | 80 | func (c *SystemConfig) ExternalDiskDir() string { 81 | return c.get("external_disk_dir") 82 | } 83 | 84 | func (c *SystemConfig) InternalDiskDir() string { 85 | return c.get("internal_disk_dir") 86 | } 87 | 88 | func (c *SystemConfig) get(key string) string { 89 | value, err := c.parser.GetInterpolated("platform", key) 90 | if err != nil { 91 | log.Fatal(err) 92 | } 93 | return value 94 | } 95 | -------------------------------------------------------------------------------- /backend/config/system_config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestSystemConfigInterpolation(t *testing.T) { 13 | db := path.Join(t.TempDir(), "db") 14 | content := ` 15 | [platform] 16 | app_dir: test 17 | config_dir: %(app_dir)s/dir 18 | ` 19 | 20 | err := os.WriteFile(db, []byte(content), 0644) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | 25 | defer func() { _ = os.Remove(db) }() 26 | config := NewSystemConfig(db) 27 | config.Load() 28 | dir := config.ConfigDir() 29 | assert.Equal(t, "test/dir", dir) 30 | } 31 | -------------------------------------------------------------------------------- /backend/connection/internet.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | type InternetChecker interface { 9 | Check() error 10 | } 11 | 12 | type Internet struct { 13 | } 14 | 15 | func NewInternetChecker() InternetChecker { 16 | return &Internet{} 17 | } 18 | 19 | func (i *Internet) Check() error { 20 | url := "http://apps.syncloud.org/releases/stable/index" 21 | response, err := http.Get(url) 22 | if err != nil { 23 | return fmt.Errorf("internet check url %s is not reachable, error: %s", url, err) 24 | } 25 | if response.StatusCode != 200 { 26 | return fmt.Errorf("internet check, response status_code: %d", response.StatusCode) 27 | } 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /backend/cron/certificate_job.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "github.com/syncloud/platform/cert" 5 | ) 6 | 7 | type CertificateJob struct { 8 | certGenerator cert.Generator 9 | } 10 | 11 | func NewCertificateJob(certGenerator cert.Generator) *CertificateJob { 12 | return &CertificateJob{ 13 | certGenerator: certGenerator, 14 | } 15 | } 16 | 17 | func (j *CertificateJob) Run() error { 18 | return j.certGenerator.Generate() 19 | } 20 | -------------------------------------------------------------------------------- /backend/cron/external_address_job.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "github.com/syncloud/platform/access" 5 | ) 6 | 7 | type ExternalAddressJob struct { 8 | externalAddress *access.ExternalAddress 9 | } 10 | 11 | func NewExternalAddressJob(externalAddress *access.ExternalAddress) *ExternalAddressJob { 12 | return &ExternalAddressJob{ 13 | externalAddress: externalAddress, 14 | } 15 | } 16 | 17 | func (j *ExternalAddressJob) Run() error { 18 | return j.externalAddress.Sync() 19 | } 20 | -------------------------------------------------------------------------------- /backend/cron/runner.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "github.com/syncloud/platform/config" 5 | "log" 6 | "time" 7 | ) 8 | 9 | type Cron struct { 10 | jobs []Job 11 | delay time.Duration 12 | userConfig *config.UserConfig 13 | } 14 | 15 | type Job interface { 16 | Run() error 17 | } 18 | 19 | func New(jobs []Job, delay time.Duration, userConfig *config.UserConfig) *Cron { 20 | return &Cron{jobs: jobs, delay: delay, userConfig: userConfig} 21 | } 22 | 23 | func (c *Cron) StartSingle() { 24 | if !c.userConfig.IsActivated() { 25 | log.Println("device is not activated yet, not running cron") 26 | return 27 | } 28 | for _, job := range c.jobs { 29 | err := job.Run() 30 | if err != nil { 31 | log.Printf("Cron job failed: %s", err) 32 | } 33 | } 34 | 35 | } 36 | 37 | func (c *Cron) Start() error { 38 | go func() { 39 | for { 40 | c.StartSingle() 41 | time.Sleep(c.delay) 42 | } 43 | }() 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /backend/cron/scheduler.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import "time" 4 | 5 | type SimpleScheduler struct { 6 | } 7 | 8 | func NewSimpleScheduler() *SimpleScheduler { 9 | return &SimpleScheduler{} 10 | } 11 | func (s *SimpleScheduler) ShouldRun(day int, hour int, now time.Time, last time.Time) bool { 12 | if now.Truncate(time.Hour) == last.Truncate(time.Hour) { 13 | return false 14 | } 15 | if day == 0 { 16 | return now.Hour() == hour 17 | } else { 18 | if s.weekDay(now) == day { 19 | return now.Hour() == hour 20 | } 21 | return false 22 | } 23 | } 24 | 25 | func (s *SimpleScheduler) weekDay(now time.Time) int { 26 | weekday := now.Weekday() 27 | if weekday == time.Sunday { 28 | return 7 29 | } 30 | return int(weekday) 31 | } 32 | -------------------------------------------------------------------------------- /backend/cron/scheduler_test.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | var monday0am = time.Date(2022, 11, 21, 0, 0, 0, 0, time.UTC) 10 | 11 | func TestShouldRun_GoodDay_WrongHour_FirstTime_NotRun(t *testing.T) { 12 | assert.False(t, NewSimpleScheduler().ShouldRun(1, 1, monday0am, time.Time{})) 13 | } 14 | 15 | func TestShouldRun_GoodDay_GoodHour_FirstTime_Run(t *testing.T) { 16 | assert.True(t, NewSimpleScheduler().ShouldRun(1, 0, monday0am, time.Time{})) 17 | } 18 | 19 | func TestShouldRun_GoodDay_WrongHour_SecondTime_NotRun(t *testing.T) { 20 | assert.False(t, NewSimpleScheduler().ShouldRun(1, 1, monday0am, monday0am)) 21 | } 22 | 23 | func TestShouldRun_GoodDay_GoodHour_SecondTime_NotRun(t *testing.T) { 24 | assert.True(t, NewSimpleScheduler().ShouldRun(1, 1, monday0am.Add(1*time.Hour), monday0am)) 25 | } 26 | 27 | func TestShouldRun_WrongDay_GoodHour_FirstTime_NotRun(t *testing.T) { 28 | assert.False(t, NewSimpleScheduler().ShouldRun(1, 1, monday0am.Add(25*time.Hour), time.Time{})) 29 | } 30 | 31 | func TestShouldRun_WrongDay_GoodHour_SecondTime_NotRun(t *testing.T) { 32 | assert.False(t, NewSimpleScheduler().ShouldRun(1, 1, monday0am.Add(25*time.Hour), monday0am)) 33 | } 34 | 35 | func TestShouldRun_EveryDay_WrongHour_FirstTime_NotRun(t *testing.T) { 36 | assert.False(t, NewSimpleScheduler().ShouldRun(0, 1, monday0am, time.Time{})) 37 | } 38 | 39 | func TestShouldRun_EveryDay_GoodHour_SecondTime_NotRun(t *testing.T) { 40 | assert.False(t, NewSimpleScheduler().ShouldRun(0, 0, monday0am, monday0am)) 41 | } 42 | 43 | func TestShouldRun_EveryDay_GoodHour_FirstTime_Run(t *testing.T) { 44 | assert.True(t, NewSimpleScheduler().ShouldRun(0, 0, monday0am, time.Time{})) 45 | } 46 | 47 | func TestShouldRun_Sunday_GoodHour_FirstTime_Run(t *testing.T) { 48 | assert.True(t, NewSimpleScheduler().ShouldRun(7, 0, monday0am.AddDate(0, 0, 6), time.Time{})) 49 | } 50 | -------------------------------------------------------------------------------- /backend/cron/time_sync_job.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "github.com/syncloud/platform/cli" 5 | "github.com/syncloud/platform/date" 6 | "go.uber.org/zap" 7 | "time" 8 | ) 9 | 10 | type TimeSyncJob struct { 11 | executor cli.Executor 12 | lastRun time.Time 13 | dateProvider date.Provider 14 | logger *zap.Logger 15 | } 16 | 17 | func NewTimeSyncJob(executor cli.Executor, dateProvider date.Provider, logger *zap.Logger) *TimeSyncJob { 18 | return &TimeSyncJob{ 19 | executor: executor, 20 | dateProvider: dateProvider, 21 | logger: logger, 22 | } 23 | } 24 | 25 | func (t *TimeSyncJob) Run() error { 26 | now := t.dateProvider.Now() 27 | if t.lastRun.Add(time.Hour * 24).Before(now) { 28 | output, err := t.executor.CombinedOutput("service", "ntp", "stop") 29 | t.logger.Info(string(output)) 30 | if err != nil { 31 | return err 32 | } 33 | output, err = t.executor.CombinedOutput("ntpd", "-gq") 34 | t.logger.Info(string(output)) 35 | if err != nil { 36 | return err 37 | } 38 | output, err = t.executor.CombinedOutput("service", "ntp", "start") 39 | t.logger.Info(string(output)) 40 | if err != nil { 41 | return err 42 | } 43 | t.lastRun = now 44 | } 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /backend/cron/time_sync_job_test.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "github.com/syncloud/platform/log" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | type ExecutorStub struct { 11 | called int 12 | } 13 | 14 | func (e *ExecutorStub) CombinedOutput(_ string, _ ...string) ([]byte, error) { 15 | e.called++ 16 | return []byte("test output"), nil 17 | } 18 | 19 | type DateProviderStub struct { 20 | now time.Time 21 | } 22 | 23 | func (d *DateProviderStub) Now() time.Time { 24 | return d.now 25 | } 26 | 27 | func TestTimeSyncJob_RunOnce_24h(t *testing.T) { 28 | executor := &ExecutorStub{} 29 | date := &DateProviderStub{} 30 | job := NewTimeSyncJob(executor, date, log.Default()) 31 | date.now = time.Now() 32 | err := job.Run() 33 | assert.NoError(t, err) 34 | date.now = date.now.Add(time.Hour * 24) 35 | err = job.Run() 36 | err = job.Run() 37 | assert.NoError(t, err) 38 | assert.Equal(t, 3, executor.called) 39 | } 40 | 41 | func TestTimeSyncJob_RunTwice_48h(t *testing.T) { 42 | executor := &ExecutorStub{} 43 | date := &DateProviderStub{} 44 | job := NewTimeSyncJob(executor, date, log.Default()) 45 | date.now = time.Now() 46 | err := job.Run() 47 | assert.NoError(t, err) 48 | date.now = date.now.Add(time.Hour * 48) 49 | err = job.Run() 50 | err = job.Run() 51 | assert.NoError(t, err) 52 | assert.Equal(t, 6, executor.called) 53 | } 54 | -------------------------------------------------------------------------------- /backend/date/provider.go: -------------------------------------------------------------------------------- 1 | package date 2 | 3 | import "time" 4 | 5 | type RealProvider struct { 6 | } 7 | 8 | type Provider interface { 9 | Now() time.Time 10 | } 11 | 12 | func New() *RealProvider { 13 | return &RealProvider{} 14 | } 15 | 16 | func (d *RealProvider) Now() time.Time { 17 | return time.Now() 18 | } 19 | -------------------------------------------------------------------------------- /backend/date/provider_test.go: -------------------------------------------------------------------------------- 1 | package date 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestRealProvider_Now(t *testing.T) { 9 | provider := New() 10 | assert.Greater(t, provider.Now().Year(), 2000) 11 | } 12 | -------------------------------------------------------------------------------- /backend/du/disk_usage.go: -------------------------------------------------------------------------------- 1 | package du 2 | 3 | import ( 4 | "github.com/syncloud/platform/cli" 5 | "regexp" 6 | "strconv" 7 | ) 8 | 9 | type DiskUsage interface { 10 | Used(path string) (uint64, error) 11 | } 12 | 13 | type ShellDiskUsage struct { 14 | executor cli.Executor 15 | } 16 | 17 | func New(executor cli.Executor) *ShellDiskUsage { 18 | return &ShellDiskUsage{executor} 19 | } 20 | 21 | func (d *ShellDiskUsage) Used(path string) (uint64, error) { 22 | out, err := d.executor.CombinedOutput("du", "-s", path) 23 | if err != nil { 24 | return 0, err 25 | } 26 | r, err := regexp.Compile(`(\d+).*`) 27 | if err != nil { 28 | return 0, err 29 | } 30 | match := r.FindStringSubmatch(string(out)) 31 | i, err := strconv.ParseUint(match[1], 10, 64) 32 | if err != nil { 33 | return 0, err 34 | } 35 | return i, nil 36 | } 37 | -------------------------------------------------------------------------------- /backend/du/disk_usage_test.go: -------------------------------------------------------------------------------- 1 | package du 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | type ExecutorStub struct { 9 | output string 10 | } 11 | 12 | func (e *ExecutorStub) CombinedOutput(_ string, _ ...string) ([]byte, error) { 13 | return []byte(e.output), nil 14 | } 15 | 16 | func TestShellDiskUsage_Used(t *testing.T) { 17 | output := "125 ." 18 | 19 | usage := New(&ExecutorStub{output: output}) 20 | 21 | bytes, err := usage.Used("") 22 | assert.Nil(t, err) 23 | assert.Equal(t, uint64(125), bytes) 24 | 25 | } 26 | -------------------------------------------------------------------------------- /backend/event/trigger.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "github.com/syncloud/platform/snap/model" 5 | "go.uber.org/zap" 6 | ) 7 | 8 | type Trigger struct { 9 | snapServer SnapServer 10 | snapCli SnapRunner 11 | logger *zap.Logger 12 | } 13 | 14 | type SnapServer interface { 15 | Snaps() ([]model.Snap, error) 16 | } 17 | 18 | type SnapRunner interface { 19 | Run(name string) error 20 | } 21 | 22 | func New(snapServer SnapServer, snapCli SnapRunner, logger *zap.Logger) *Trigger { 23 | return &Trigger{ 24 | snapServer: snapServer, 25 | snapCli: snapCli, 26 | logger: logger, 27 | } 28 | } 29 | 30 | func (t *Trigger) RunAccessChangeEvent() error { 31 | return t.RunEventOnAllApps("access-change") 32 | } 33 | 34 | func (t *Trigger) RunCertificateChangeEvent() error { 35 | return t.RunEventOnAllApps("certificate-change") 36 | } 37 | 38 | func (t *Trigger) RunDiskChangeEvent() error { 39 | return t.RunEventOnAllApps("storage-change") 40 | } 41 | 42 | func (t *Trigger) RunEventOnAllApps(command string) error { 43 | 44 | snaps, err := t.snapServer.Snaps() 45 | if err != nil { 46 | return err 47 | } 48 | for _, app := range snaps { 49 | cmd := app.FindCommand(command) 50 | if cmd != nil { 51 | err = t.snapCli.Run(cmd.FullName()) 52 | if err != nil { 53 | return err 54 | } 55 | } 56 | } 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /backend/event/trigger_test.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "github.com/syncloud/platform/log" 6 | "github.com/syncloud/platform/snap/model" 7 | "testing" 8 | ) 9 | 10 | type SnapServerStub struct { 11 | snaps []model.Snap 12 | } 13 | 14 | func (s SnapServerStub) Snaps() ([]model.Snap, error) { 15 | return s.snaps, nil 16 | } 17 | 18 | type SnapCliStub struct { 19 | runs []string 20 | } 21 | 22 | func (e *SnapCliStub) Run(name string) error { 23 | e.runs = append(e.runs, name) 24 | return nil 25 | } 26 | 27 | func TestEvent_All(t *testing.T) { 28 | snapCli := &SnapCliStub{} 29 | snapd := &SnapServerStub{ 30 | snaps: []model.Snap{ 31 | { 32 | Name: "app1", Summary: "", 33 | Apps: []model.App{ 34 | {Name: "event1", Snap: "app1"}, 35 | }, 36 | }, 37 | { 38 | Name: "app2", Summary: "", 39 | Apps: []model.App{ 40 | {Name: "event1", Snap: "app2"}, 41 | {Name: "event2", Snap: "app2"}, 42 | }, 43 | }, 44 | }, 45 | } 46 | trigger := New(snapd, snapCli, log.Default()) 47 | err := trigger.RunEventOnAllApps("event1") 48 | assert.Nil(t, err) 49 | assert.Len(t, snapCli.runs, 2) 50 | assert.Contains(t, snapCli.runs, "app1.event1") 51 | assert.Contains(t, snapCli.runs, "app2.event1") 52 | } 53 | 54 | func TestEvent_Filter(t *testing.T) { 55 | snapCli := &SnapCliStub{} 56 | snapd := &SnapServerStub{ 57 | snaps: []model.Snap{ 58 | { 59 | Name: "app1", Summary: "", 60 | Apps: []model.App{ 61 | {Name: "event1", Snap: "app1"}, 62 | }, 63 | }, 64 | { 65 | Name: "app2", Summary: "", 66 | Apps: []model.App{ 67 | {Name: "event1", Snap: "app2"}, 68 | {Name: "event2", Snap: "app2"}, 69 | }, 70 | }, 71 | }, 72 | } 73 | trigger := New(snapd, snapCli, log.Default()) 74 | err := trigger.RunEventOnAllApps("event2") 75 | assert.Nil(t, err) 76 | assert.Len(t, snapCli.runs, 1) 77 | assert.Contains(t, snapCli.runs, "app2.event2") 78 | } 79 | -------------------------------------------------------------------------------- /backend/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/syncloud/platform 2 | 3 | require ( 4 | github.com/bigkevmcd/go-configparser v0.0.0-20210106142102-909504547ead 5 | github.com/go-acme/lego/v4 v4.20.4 6 | github.com/go-ldap/ldap/v3 v3.4.4 7 | github.com/golobby/container/v3 v3.3.1 8 | github.com/google/uuid v1.6.0 9 | github.com/gorilla/mux v1.8.0 10 | github.com/gorilla/sessions v1.2.1 11 | github.com/hashicorp/go-retryablehttp v0.7.7 12 | github.com/mattn/go-sqlite3 v1.14.7 13 | github.com/otiai10/copy v1.7.0 14 | github.com/prometheus/procfs v0.7.3 15 | github.com/ricochet2200/go-disk-usage/du v0.0.0-20210707232629-ac9918953285 16 | github.com/spf13/cobra v1.1.1 17 | github.com/stretchr/testify v1.9.0 18 | github.com/syncloud/golib v1.1.15 19 | go.uber.org/zap v1.25.0 20 | golang.org/x/exp v0.0.0-20240213143201-ec583247a57a 21 | gopkg.in/yaml.v3 v3.0.1 22 | ) 23 | 24 | require ( 25 | github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect 26 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 27 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 28 | github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect 29 | github.com/go-jose/go-jose/v4 v4.0.4 // indirect 30 | github.com/gorilla/securecookie v1.1.1 // indirect 31 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 32 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 33 | github.com/miekg/dns v1.1.62 // indirect 34 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 35 | github.com/spf13/pflag v1.0.5 // indirect 36 | go.uber.org/multierr v1.10.0 // indirect 37 | golang.org/x/crypto v0.28.0 // indirect 38 | golang.org/x/mod v0.21.0 // indirect 39 | golang.org/x/net v0.30.0 // indirect 40 | golang.org/x/sync v0.8.0 // indirect 41 | golang.org/x/sys v0.26.0 // indirect 42 | golang.org/x/text v0.19.0 // indirect 43 | golang.org/x/tools v0.25.0 // indirect 44 | ) 45 | 46 | go 1.22.0 47 | 48 | toolchain go1.23.2 49 | -------------------------------------------------------------------------------- /backend/http/client.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type Client interface { 8 | Post(url, bodyType string, body interface{}) (*http.Response, error) 9 | Get(url string) (*http.Response, error) 10 | } 11 | -------------------------------------------------------------------------------- /backend/identification/parser.go: -------------------------------------------------------------------------------- 1 | package identification 2 | 3 | import ( 4 | "github.com/bigkevmcd/go-configparser" 5 | "log" 6 | "net" 7 | ) 8 | 9 | const DefaultIdFile = "/etc/syncloud/id.cfg" 10 | 11 | type Id struct { 12 | Name string `json:"name"` 13 | Title string `json:"title"` 14 | MacAddress string `json:"mac_address"` 15 | } 16 | 17 | type Parser struct { 18 | filename string 19 | } 20 | 21 | type IdParser interface { 22 | Id() (*Id, error) 23 | } 24 | 25 | func New() *Parser { 26 | return &Parser{filename: DefaultIdFile} 27 | } 28 | 29 | func (p *Parser) get(key string, def string) string { 30 | config, err := configparser.NewConfigParserFromFile(p.filename) 31 | if err != nil { 32 | log.Printf("cannot load id config: %s, %s", p.filename, err) 33 | config = configparser.New() 34 | } 35 | 36 | option, err := config.HasOption("id", key) 37 | if err != nil { 38 | log.Printf("identification key (%s) error: %s", key, err) 39 | return def 40 | } 41 | if option { 42 | option, err := config.Get("id", key) 43 | if err != nil { 44 | log.Printf("identification key (%s) error: %s", key, err) 45 | return def 46 | } 47 | return option 48 | } 49 | return def 50 | } 51 | 52 | func (p *Parser) name() string { 53 | return p.get("name", "unknown") 54 | } 55 | 56 | func (p *Parser) title() string { 57 | return p.get("title", "Unknown") 58 | } 59 | 60 | func (p *Parser) Id() (*Id, error) { 61 | mac, err := GetMac() 62 | if err != nil { 63 | return nil, err 64 | } 65 | return &Id{p.name(), p.title(), mac}, nil 66 | } 67 | 68 | func GetMac() (string, error) { 69 | ifas, err := net.Interfaces() 70 | if err != nil { 71 | return "", err 72 | } 73 | for _, ifa := range ifas { 74 | addr := ifa.HardwareAddr.String() 75 | if len(ifa.HardwareAddr) >= 6 && ifa.Name != "" { 76 | return addr, nil 77 | } 78 | } 79 | return "", nil 80 | 81 | } 82 | -------------------------------------------------------------------------------- /backend/identification/parser_test.go: -------------------------------------------------------------------------------- 1 | package identification 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "os" 6 | "path" 7 | "testing" 8 | ) 9 | 10 | func TestMac(t *testing.T) { 11 | mac, err := GetMac() 12 | assert.Nil(t, err) 13 | assert.NotEmpty(t, mac) 14 | } 15 | 16 | func TestParser_Id(t *testing.T) { 17 | tempDir := t.TempDir() 18 | idFile := path.Join(tempDir, "id") 19 | err := os.WriteFile(idFile, []byte(` 20 | [id] 21 | name=name 22 | title=title 23 | `), 0644) 24 | assert.NoError(t, err) 25 | parser := &Parser{filename: idFile} 26 | id, err := parser.Id() 27 | assert.NoError(t, err) 28 | assert.Equal(t, "name", id.Name) 29 | assert.Equal(t, "title", id.Title) 30 | } 31 | 32 | func TestParser_Id_NoIdSection(t *testing.T) { 33 | tempDir := t.TempDir() 34 | idFile := path.Join(tempDir, "id") 35 | err := os.WriteFile(idFile, []byte(` 36 | name=name 37 | title=title 38 | `), 0644) 39 | assert.NoError(t, err) 40 | parser := &Parser{filename: idFile} 41 | id, err := parser.Id() 42 | assert.NoError(t, err) 43 | assert.Equal(t, "unknown", id.Name) 44 | assert.Equal(t, "Unknown", id.Title) 45 | } 46 | -------------------------------------------------------------------------------- /backend/installer/installer.go: -------------------------------------------------------------------------------- 1 | package installer 2 | 3 | import ( 4 | "log" 5 | "os/exec" 6 | ) 7 | 8 | type Installer struct { 9 | } 10 | 11 | const ( 12 | UpgradeCmd = "/snap/platform/current/bin/upgrade-snapd.sh" 13 | ) 14 | 15 | func New() *Installer { 16 | return &Installer{} 17 | } 18 | 19 | func (installer *Installer) Upgrade() error { 20 | log.Println("Running installer upgrade", UpgradeCmd) 21 | out, err := exec.Command(UpgradeCmd).CombinedOutput() 22 | log.Printf("Installer upgrade output %s", out) 23 | if err != nil { 24 | return err 25 | } 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /backend/installer/model.go: -------------------------------------------------------------------------------- 1 | package installer 2 | 3 | type AppInstaller interface { 4 | Upgrade() error 5 | } 6 | -------------------------------------------------------------------------------- /backend/ioc/common_test.go: -------------------------------------------------------------------------------- 1 | package ioc 2 | 3 | import ( 4 | "github.com/syncloud/platform/config" 5 | "log" 6 | "os" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestInit(t *testing.T) { 13 | configDb, err := os.CreateTemp("", "") 14 | _ = os.Remove(configDb.Name()) 15 | assert.Nil(t, err) 16 | systemConfig, err := os.CreateTemp("", "") 17 | assert.Nil(t, err) 18 | content := ` 19 | [platform] 20 | app_dir: test 21 | data_dir: test 22 | config_dir: test 23 | ` 24 | err = os.WriteFile(systemConfig.Name(), []byte(content), 0644) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | backupDir := t.TempDir() 30 | varDir := t.TempDir() 31 | 32 | c, err := Init(configDb.Name(), systemConfig.Name(), backupDir, varDir) 33 | assert.NoError(t, err) 34 | var conf *config.SystemConfig 35 | err = c.Resolve(&conf) 36 | assert.NoError(t, err) 37 | } 38 | -------------------------------------------------------------------------------- /backend/ioc/internal_api.go: -------------------------------------------------------------------------------- 1 | package ioc 2 | 3 | import ( 4 | "github.com/golobby/container/v3" 5 | "github.com/syncloud/platform/auth" 6 | "github.com/syncloud/platform/config" 7 | "github.com/syncloud/platform/rest" 8 | "github.com/syncloud/platform/storage" 9 | "github.com/syncloud/platform/systemd" 10 | ) 11 | 12 | func InitInternalApi(userConfig string, systemConfig string, backupDir string, varDir string, network string, address string) (container.Container, error) { 13 | c, err := Init(userConfig, systemConfig, backupDir, varDir) 14 | if err != nil { 15 | return nil, err 16 | } 17 | err = c.Singleton(func( 18 | userConfig *config.UserConfig, 19 | storage *storage.Storage, 20 | systemd *systemd.Control, 21 | middleware *rest.Middleware, 22 | authelia *auth.Authelia, 23 | ) *rest.Api { 24 | return rest.NewApi(userConfig, storage, systemd, middleware, network, address, authelia, logger) 25 | }) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | err = c.Singleton(func( 31 | api *rest.Api, 32 | ) []Service { 33 | return []Service{ 34 | api, 35 | } 36 | }) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return c, nil 41 | } 42 | -------------------------------------------------------------------------------- /backend/ioc/internal_api_test.go: -------------------------------------------------------------------------------- 1 | package ioc 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestInitInternalApi(t *testing.T) { 12 | configDb, err := os.CreateTemp("", "") 13 | _ = os.Remove(configDb.Name()) 14 | assert.Nil(t, err) 15 | systemConfig, err := os.CreateTemp("", "") 16 | assert.Nil(t, err) 17 | content := ` 18 | [platform] 19 | app_dir: test 20 | data_dir: test 21 | config_dir: test 22 | ` 23 | err = os.WriteFile(systemConfig.Name(), []byte(content), 0644) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | 28 | backupDir := t.TempDir() 29 | varDir := t.TempDir() 30 | 31 | c, err := InitInternalApi(configDb.Name(), systemConfig.Name(), backupDir, varDir, "", "") 32 | assert.NoError(t, err) 33 | var services []Service 34 | err = c.Resolve(&services) 35 | assert.Nil(t, err) 36 | assert.Len(t, services, 1) 37 | } 38 | -------------------------------------------------------------------------------- /backend/ioc/public_api_test.go: -------------------------------------------------------------------------------- 1 | package ioc 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestInitPublicApi(t *testing.T) { 12 | configDb, err := os.CreateTemp("", "") 13 | _ = os.Remove(configDb.Name()) 14 | assert.Nil(t, err) 15 | systemConfig, err := os.CreateTemp("", "") 16 | assert.Nil(t, err) 17 | content := ` 18 | [platform] 19 | app_dir: test 20 | data_dir: test 21 | config_dir: test 22 | ` 23 | err = os.WriteFile(systemConfig.Name(), []byte(content), 0644) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | 28 | backupDir := t.TempDir() 29 | varDir := t.TempDir() 30 | 31 | c, err := InitPublicApi(configDb.Name(), systemConfig.Name(), backupDir, varDir, "", "") 32 | assert.NoError(t, err) 33 | var services []Service 34 | err = c.Resolve(&services) 35 | assert.Nil(t, err) 36 | assert.Len(t, services, 4) 37 | 38 | } 39 | -------------------------------------------------------------------------------- /backend/ioc/service.go: -------------------------------------------------------------------------------- 1 | package ioc 2 | 3 | import "github.com/golobby/container/v3" 4 | 5 | type Service interface { 6 | Start() error 7 | } 8 | 9 | func Start(c container.Container) error { 10 | return c.Call(func(services []Service) error { 11 | for _, service := range services { 12 | err := service.Start() 13 | if err != nil { 14 | return err 15 | } 16 | } 17 | return nil 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /backend/job/master.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | type Job func() error 9 | 10 | type SingleJobMaster struct { 11 | mutex *sync.Mutex 12 | status int 13 | job Job 14 | name string 15 | } 16 | 17 | func NewMaster() *SingleJobMaster { 18 | return &SingleJobMaster{ 19 | mutex: &sync.Mutex{}, 20 | status: Idle, 21 | } 22 | } 23 | 24 | func (m *SingleJobMaster) Status() Status { 25 | m.mutex.Lock() 26 | defer m.mutex.Unlock() 27 | return NewStatus(m.name, m.status) 28 | } 29 | 30 | func (m *SingleJobMaster) Offer(name string, job Job) error { 31 | m.mutex.Lock() 32 | defer m.mutex.Unlock() 33 | if m.status == Idle { 34 | m.status = Waiting 35 | m.job = job 36 | m.name = name 37 | return nil 38 | } else { 39 | return fmt.Errorf("busy") 40 | } 41 | } 42 | 43 | func (m *SingleJobMaster) Take() Job { 44 | 45 | m.mutex.Lock() 46 | defer m.mutex.Unlock() 47 | if m.status == Waiting { 48 | m.status = Busy 49 | return m.job 50 | } else { 51 | return nil 52 | } 53 | } 54 | 55 | func (m *SingleJobMaster) Complete() error { 56 | m.mutex.Lock() 57 | defer m.mutex.Unlock() 58 | if m.status == Busy { 59 | m.status = Idle 60 | m.job = nil 61 | m.name = "" 62 | return nil 63 | } else { 64 | return fmt.Errorf("nothing to complete") 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /backend/job/status.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | const ( 4 | Idle int = iota 5 | Waiting 6 | Busy 7 | ) 8 | 9 | type Status struct { 10 | Name string `json:"name"` 11 | Status string `json:"status"` 12 | } 13 | 14 | func NewStatus(name string, status int) Status { 15 | return Status{ 16 | Name: name, 17 | Status: []string{"Idle", "Waiting", "Busy"}[status], 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/job/worker.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "time" 6 | ) 7 | 8 | type Master interface { 9 | Take() Job 10 | Complete() error 11 | } 12 | 13 | type Worker struct { 14 | master Master 15 | logger *zap.Logger 16 | } 17 | 18 | func NewWorker(master Master, logger *zap.Logger) *Worker { 19 | return &Worker{master, logger} 20 | } 21 | 22 | func (w *Worker) Start() { 23 | for { 24 | if !w.Do() { 25 | time.Sleep(1 * time.Second) 26 | } 27 | } 28 | } 29 | 30 | func (w *Worker) Do() bool { 31 | job := w.master.Take() 32 | if job == nil { 33 | return false 34 | } 35 | err := job() 36 | if err != nil { 37 | w.logger.Error("error in the task", zap.Error(err)) 38 | } 39 | err = w.master.Complete() 40 | if err != nil { 41 | w.logger.Error("cannot complete task", zap.Error(err)) 42 | } 43 | return true 44 | } 45 | -------------------------------------------------------------------------------- /backend/job/worker_test.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "github.com/syncloud/platform/log" 6 | "testing" 7 | ) 8 | 9 | type MasterStub struct { 10 | job Job 11 | taken int 12 | completed int 13 | } 14 | 15 | func (m *MasterStub) Take() Job { 16 | m.taken++ 17 | return m.job 18 | } 19 | 20 | func (m *MasterStub) Complete() error { 21 | m.completed++ 22 | return nil 23 | } 24 | 25 | func TestJob(t *testing.T) { 26 | master := &MasterStub{} 27 | worker := NewWorker(master, log.Default()) 28 | 29 | ran := false 30 | master.job = func() error { 31 | ran = true 32 | return nil 33 | } 34 | worker.Do() 35 | 36 | assert.True(t, ran) 37 | assert.Equal(t, 1, master.completed) 38 | 39 | } 40 | 41 | func TestNoJob(t *testing.T) { 42 | master := &MasterStub{} 43 | worker := NewWorker(master, log.Default()) 44 | 45 | master.job = nil 46 | worker.Do() 47 | 48 | assert.Equal(t, 0, master.completed) 49 | } 50 | -------------------------------------------------------------------------------- /backend/log/category.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | const ( 4 | CategoryKey = "category" 5 | CategoryCertificate = "certificate" 6 | ) 7 | -------------------------------------------------------------------------------- /backend/log/default.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "go.uber.org/zap" 6 | ) 7 | 8 | func Default() *zap.Logger { 9 | logConfig := zap.NewProductionConfig() 10 | logConfig.Encoding = "console" 11 | logConfig.EncoderConfig.TimeKey = "" 12 | logConfig.EncoderConfig.ConsoleSeparator = " " 13 | logger, err := logConfig.Build() 14 | if err != nil { 15 | panic(fmt.Sprintf("can't initialize zap logger: %v", err)) 16 | } 17 | return logger 18 | } 19 | -------------------------------------------------------------------------------- /backend/network/iface.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | type Interface struct { 4 | Name string `json:"name"` 5 | Addresses []string `json:"addresses"` 6 | } 7 | -------------------------------------------------------------------------------- /backend/network/interfaces_test.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestIPv4(t *testing.T) { 9 | 10 | ip, err := New().LocalIPv4() 11 | assert.True(t, err != nil || ip != nil) 12 | } 13 | 14 | func TestIPv6(t *testing.T) { 15 | 16 | interfaces := New() 17 | _, _ = interfaces.IPv6() 18 | assert.True(t, true) 19 | } 20 | 21 | func TestList(t *testing.T) { 22 | 23 | list, err := New().List() 24 | assert.Nil(t, err) 25 | 26 | assert.Greater(t, len(list), 0) 27 | } 28 | -------------------------------------------------------------------------------- /backend/nginx/service.go: -------------------------------------------------------------------------------- 1 | package nginx 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "strings" 7 | ) 8 | 9 | type Systemd interface { 10 | ReloadService(service string) error 11 | } 12 | 13 | type SystemConfig interface { 14 | ConfigDir() string 15 | DataDir() string 16 | } 17 | 18 | type UserConfig interface { 19 | GetDeviceDomain() string 20 | } 21 | 22 | type Nginx struct { 23 | systemd Systemd 24 | systemConfig SystemConfig 25 | userConfig UserConfig 26 | } 27 | 28 | func New(systemd Systemd, systemConfig SystemConfig, userConfig UserConfig) *Nginx { 29 | return &Nginx{ 30 | systemd: systemd, 31 | userConfig: userConfig, 32 | systemConfig: systemConfig, 33 | } 34 | } 35 | 36 | func (n *Nginx) ReloadPublic() error { 37 | return n.systemd.ReloadService("platform.nginx-public") 38 | } 39 | 40 | func (n *Nginx) InitConfig() error { 41 | domain := n.userConfig.GetDeviceDomain() 42 | configDir := n.systemConfig.ConfigDir() 43 | templateFile, err := os.ReadFile(path.Join(configDir, "nginx", "public.conf")) 44 | if err != nil { 45 | return err 46 | } 47 | template := string(templateFile) 48 | template = strings.ReplaceAll(template, "{{ domain_regex }}", strings.ReplaceAll(domain, ".", "\\.")) 49 | template = strings.ReplaceAll(template, "{{ domain }}", domain) 50 | nginxConfigDir := n.systemConfig.DataDir() 51 | nginxConfigFile := path.Join(nginxConfigDir, "nginx.conf") 52 | err = os.WriteFile(nginxConfigFile, []byte(template), 0644) 53 | return err 54 | } 55 | -------------------------------------------------------------------------------- /backend/nginx/service_test.go: -------------------------------------------------------------------------------- 1 | package nginx 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type SystemdMock struct { 12 | } 13 | 14 | func (s *SystemdMock) ReloadService(_ string) error { 15 | return nil 16 | } 17 | 18 | type SystemConfigMock struct { 19 | configDir string 20 | dataDir string 21 | } 22 | 23 | func (s *SystemConfigMock) ConfigDir() string { 24 | return s.configDir 25 | } 26 | 27 | func (s *SystemConfigMock) DataDir() string { 28 | return s.dataDir 29 | } 30 | 31 | type UserConfigMock struct { 32 | deviceDomain string 33 | } 34 | 35 | func (u *UserConfigMock) GetDeviceDomain() string { 36 | return u.deviceDomain 37 | } 38 | 39 | func TestSubstitution(t *testing.T) { 40 | 41 | outputDir := t.TempDir() 42 | 43 | configDir := path.Join("..", "..", "config") 44 | systemd := &SystemdMock{} 45 | systemConfig := &SystemConfigMock{configDir: configDir, dataDir: outputDir} 46 | userConfig := &UserConfigMock{"example.com"} 47 | nginx := New(systemd, systemConfig, userConfig) 48 | err := nginx.InitConfig() 49 | assert.Nil(t, err) 50 | resultFile := path.Join(outputDir, "nginx.conf") 51 | 52 | contents, err := os.ReadFile(resultFile) 53 | assert.Nil(t, err) 54 | 55 | assert.Contains(t, string(contents), "server_name example.com;") 56 | assert.Contains(t, string(contents), "server_name ~^(.*\\.)?(?P.*)\\.example\\.com$;") 57 | } 58 | -------------------------------------------------------------------------------- /backend/parser/template.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "text/template" 8 | ) 9 | 10 | func Generate(input, output string, data interface{}) error { 11 | _, err := os.Stat(output) 12 | if errors.Is(err, os.ErrNotExist) { 13 | err := os.MkdirAll(output, 0755) 14 | if err != nil { 15 | return err 16 | } 17 | } 18 | 19 | var templates = template.Must(template.ParseGlob(fmt.Sprintf("%s/*", input))) 20 | for _, t := range templates.Templates() { 21 | err := write(output, t, data) 22 | if err != nil { 23 | return err 24 | } 25 | } 26 | return nil 27 | } 28 | 29 | func write(output string, t *template.Template, data interface{}) error { 30 | f, err := os.Create(fmt.Sprintf("%s/%s", output, t.Name())) 31 | if err != nil { 32 | return err 33 | } 34 | defer f.Close() 35 | 36 | err = t.Execute(f, data) 37 | if err != nil { 38 | return err 39 | } 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /backend/parser/template_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type Data struct { 12 | Field1 string 13 | Field2 string 14 | } 15 | 16 | func TestGenerate(t *testing.T) { 17 | 18 | tempDir := t.TempDir() 19 | outputDir := path.Join(tempDir, "dir") 20 | inputDir := path.Join("test/input") 21 | err := Generate(inputDir, outputDir, Data{ 22 | Field1: "Field1", 23 | Field2: "Field2", 24 | }) 25 | assert.Nil(t, err) 26 | 27 | actual1, err := os.ReadFile(path.Join(outputDir, "template1.txt")) 28 | assert.Nil(t, err) 29 | expected1, err := os.ReadFile("test/output/template1.txt") 30 | assert.Nil(t, err) 31 | assert.Equal(t, string(actual1), string(expected1)) 32 | 33 | actual2, err := os.ReadFile(path.Join(outputDir, "template2.txt")) 34 | assert.Nil(t, err) 35 | expected2, err := os.ReadFile("test/output/template2.txt") 36 | assert.Nil(t, err) 37 | assert.Equal(t, string(actual2), string(expected2)) 38 | } 39 | -------------------------------------------------------------------------------- /backend/parser/test/input/template1.txt: -------------------------------------------------------------------------------- 1 | test {{ .Field1 }} -------------------------------------------------------------------------------- /backend/parser/test/input/template2.txt: -------------------------------------------------------------------------------- 1 | test {{ .Field2 }} -------------------------------------------------------------------------------- /backend/parser/test/output/template1.txt: -------------------------------------------------------------------------------- 1 | test Field1 -------------------------------------------------------------------------------- /backend/parser/test/output/template2.txt: -------------------------------------------------------------------------------- 1 | test Field2 -------------------------------------------------------------------------------- /backend/redirect/passthrough_error.go: -------------------------------------------------------------------------------- 1 | package redirect 2 | 3 | type PassThroughJsonError struct { 4 | Message string 5 | Json string 6 | } 7 | 8 | func (p *PassThroughJsonError) Error() string { 9 | return p.Message 10 | } 11 | -------------------------------------------------------------------------------- /backend/rest/activate.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/syncloud/platform/activation" 6 | "github.com/syncloud/platform/rest/model" 7 | "net/http" 8 | "strings" 9 | ) 10 | 11 | type Activate struct { 12 | managed activation.ManagedActivation 13 | custom activation.CustomActivation 14 | } 15 | 16 | func NewActivateBackend(managed activation.ManagedActivation, custom activation.CustomActivation) *Activate { 17 | return &Activate{ 18 | managed: managed, 19 | custom: custom, 20 | } 21 | } 22 | 23 | func (a *Activate) Custom(req *http.Request) (interface{}, error) { 24 | var request activation.CustomActivateRequest 25 | err := json.NewDecoder(req.Body).Decode(&request) 26 | if err != nil { 27 | return nil, err 28 | } 29 | err = validate(request.DeviceUsername, request.DevicePassword) 30 | if err != nil { 31 | return nil, err 32 | } 33 | return "ok", a.custom.Activate(request.Domain, request.DeviceUsername, request.DevicePassword) 34 | } 35 | 36 | func (a *Activate) Managed(req *http.Request) (interface{}, error) { 37 | var request activation.ManagedActivateRequest 38 | err := json.NewDecoder(req.Body).Decode(&request) 39 | if err != nil { 40 | return nil, err 41 | } 42 | err = validate(request.DeviceUsername, request.DevicePassword) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return "ok", a.managed.Activate(request.RedirectEmail, request.RedirectPassword, request.Domain, request.DeviceUsername, request.DevicePassword) 47 | } 48 | 49 | func validate(username string, password string) error { 50 | if len(username) < 3 { 51 | return model.SingleParameterError("device_username", "less than 3 characters") 52 | } 53 | if len(password) < 7 { 54 | return model.SingleParameterError("device_password", "less than 7 characters") 55 | } 56 | if strings.ToLower(username) != username { 57 | return model.SingleParameterError("device_username", "use lower case username") 58 | } 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /backend/rest/certificate.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "fmt" 5 | "github.com/syncloud/platform/cert" 6 | "github.com/syncloud/platform/log" 7 | "net/http" 8 | "strings" 9 | ) 10 | 11 | type Journal interface { 12 | ReadBackend(predicate func(string) bool) []string 13 | } 14 | 15 | type Certificate struct { 16 | infoReader CertificateInfoReader 17 | journal Journal 18 | } 19 | 20 | type CertificateInfoReader interface { 21 | ReadCertificateInfo() *cert.Info 22 | } 23 | 24 | func NewCertificate(infoReader CertificateInfoReader, journal Journal) *Certificate { 25 | return &Certificate{ 26 | infoReader: infoReader, 27 | journal: journal, 28 | } 29 | } 30 | 31 | func (c *Certificate) Certificate(_ *http.Request) (interface{}, error) { 32 | return c.infoReader.ReadCertificateInfo(), nil 33 | } 34 | 35 | func (c *Certificate) CertificateLog(_ *http.Request) (interface{}, error) { 36 | return c.journal.ReadBackend(func(line string) bool { 37 | return strings.Contains(line, fmt.Sprintf(`"%s": "%s"`, log.CategoryKey, log.CategoryCertificate)) 38 | }), nil 39 | } 40 | -------------------------------------------------------------------------------- /backend/rest/handler.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type FailIfActivatedHandler struct { 8 | userConfig UserConfig 9 | handler http.Handler 10 | } 11 | 12 | func NewFailIfActivatedHandler(userConfig UserConfig, handler http.Handler) *FailIfActivatedHandler { 13 | return &FailIfActivatedHandler{ 14 | userConfig: userConfig, 15 | handler: handler, 16 | } 17 | } 18 | 19 | func (h *FailIfActivatedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 20 | if h.userConfig.IsActivated() { 21 | http.Error(w, "Device is activated", 502) 22 | return 23 | } 24 | h.handler.ServeHTTP(w, r) 25 | } 26 | -------------------------------------------------------------------------------- /backend/rest/model/app_action.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type AppActionRequest struct { 4 | AppId string `json:"app_id"` 5 | } 6 | -------------------------------------------------------------------------------- /backend/rest/model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Access struct { 4 | Ipv4 *string `json:"ipv4,omitempty"` 5 | Ipv4Enabled bool `json:"ipv4_enabled"` 6 | Ipv4Public bool `json:"ipv4_public"` 7 | AccessPort *int `json:"access_port,omitempty"` 8 | Ipv6Enabled bool `json:"ipv6_enabled"` 9 | } 10 | 11 | type RedirectInfoResponse struct { 12 | Domain string `json:"domain"` 13 | } 14 | 15 | type BackupCreateRequest struct { 16 | App string `json:"app"` 17 | } 18 | 19 | type BackupRestoreRequest struct { 20 | File string `json:"file"` 21 | } 22 | 23 | type BackupRemoveRequest struct { 24 | File string `json:"file"` 25 | } 26 | 27 | type StorageActivatePartitionRequest struct { 28 | Device string `json:"device"` 29 | Format bool `json:"format"` 30 | } 31 | 32 | type StorageActivateDisksRequest struct { 33 | Devices []string `json:"devices"` 34 | Format bool `json:"format"` 35 | } 36 | 37 | type EventTriggerRequest struct { 38 | Event string `json:"event"` 39 | } 40 | 41 | type Response struct { 42 | Success bool `json:"success"` 43 | Message string `json:"message,omitempty"` 44 | Data *interface{} `json:"data,omitempty"` 45 | ParametersMessages *[]ParameterMessages `json:"parameters_messages,omitempty"` 46 | } 47 | -------------------------------------------------------------------------------- /backend/rest/model/parameters_error.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type ParameterError struct { 9 | ParameterErrors *[]ParameterMessages 10 | } 11 | 12 | type ParameterMessages struct { 13 | Parameter string `json:"parameter,omitempty"` 14 | Messages []string `json:"messages,omitempty"` 15 | } 16 | 17 | func (pm *ParameterMessages) Error() string { 18 | return fmt.Sprintf("%s: %s", pm.Parameter, strings.Join(pm.Messages, ", ")) 19 | } 20 | 21 | func (p *ParameterError) Error() string { 22 | var errors []string 23 | for _, pm := range *p.ParameterErrors { 24 | errors = append(errors, pm.Error()) 25 | } 26 | return fmt.Sprintf("There's an error in parameters: %s", strings.Join(errors, "; ")) 27 | } 28 | 29 | func SingleParameterError(parameter string, message string) *ParameterError { 30 | return &ParameterError{ParameterErrors: &[]ParameterMessages{{ 31 | Parameter: parameter, Messages: []string{message}, 32 | }}} 33 | } 34 | -------------------------------------------------------------------------------- /backend/rest/model/service_error.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type ServiceError struct { 8 | InternalError error 9 | StatusCode int 10 | } 11 | 12 | func (e *ServiceError) Error() string { 13 | return e.InternalError.Error() 14 | } 15 | 16 | func BadRequest(err error) *ServiceError { 17 | return &ServiceError{InternalError: err, StatusCode: http.StatusBadRequest} 18 | } 19 | -------------------------------------------------------------------------------- /backend/rest/model/user_login_request.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type UserLoginRequest struct { 4 | Username string `json:"username"` 5 | Password string `json:"password"` 6 | } 7 | -------------------------------------------------------------------------------- /backend/rest/proxy.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httputil" 7 | "net/url" 8 | ) 9 | 10 | type Proxy struct { 11 | userConfig Config 12 | } 13 | 14 | type Config interface { 15 | GetRedirectApiUrl() string 16 | } 17 | 18 | func NewProxy(userConfig Config) *Proxy { 19 | return &Proxy{ 20 | userConfig: userConfig, 21 | } 22 | } 23 | 24 | func (p *Proxy) ProxyRedirect() (*httputil.ReverseProxy, error) { 25 | redirectApiUrl := p.userConfig.GetRedirectApiUrl() 26 | redirectUrl, err := url.Parse(redirectApiUrl) 27 | if err != nil { 28 | fmt.Printf("proxy url error: %v", err) 29 | return nil, err 30 | } 31 | director := func(req *http.Request) { 32 | req.URL.Scheme = redirectUrl.Scheme 33 | req.URL.Host = redirectUrl.Host 34 | req.Host = redirectUrl.Host 35 | } 36 | return &httputil.ReverseProxy{Director: director}, nil 37 | } 38 | 39 | func (p *Proxy) ProxyImage() *httputil.ReverseProxy { 40 | host := "apps.syncloud.org" 41 | director := func(req *http.Request) { 42 | query := req.URL.Query() 43 | if !query.Has("channel") { 44 | return 45 | } 46 | if !query.Has("app") { 47 | return 48 | } 49 | req.URL.Scheme = "http" 50 | req.URL.RawQuery = "" 51 | req.URL.Host = host 52 | req.URL.Path = fmt.Sprintf("/releases/%s/images/%s-128.png", query.Get("channel"), query.Get("app")) 53 | req.Host = host 54 | } 55 | return &httputil.ReverseProxy{Director: director} 56 | } 57 | 58 | func (p *Proxy) ProxyImageFunc() func(http.ResponseWriter, *http.Request) { 59 | proxy := p.ProxyImage() 60 | return func(w http.ResponseWriter, r *http.Request) { 61 | proxy.ServeHTTP(w, r) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /backend/rest/proxy_test.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type ConfigStub struct { 12 | } 13 | 14 | func (c *ConfigStub) GetRedirectApiUrl() string { 15 | return "https://proxy" 16 | } 17 | 18 | func TestProxy_ProxyImage(t *testing.T) { 19 | proxy := NewProxy(&ConfigStub{}) 20 | imageUrl, err := url.Parse("https://localhost/image?channel=stable&app=test") 21 | assert.Nil(t, err) 22 | reverseProxy := proxy.ProxyImage() 23 | req := &http.Request{URL: imageUrl} 24 | reverseProxy.Director(req) 25 | assert.Equal(t, "http://apps.syncloud.org/releases/stable/images/test-128.png", req.URL.String()) 26 | } 27 | 28 | func TestProxy_ProxyRedirect(t *testing.T) { 29 | proxy := NewProxy(&ConfigStub{}) 30 | proxyUrl, err := url.Parse("https://localhost/test?a=b") 31 | assert.Nil(t, err) 32 | reverseProxy, err := proxy.ProxyRedirect() 33 | assert.Nil(t, err) 34 | req := &http.Request{URL: proxyUrl, Host: proxyUrl.Host} 35 | reverseProxy.Director(req) 36 | assert.Equal(t, "https://proxy/test?a=b", req.URL.String()) 37 | } 38 | -------------------------------------------------------------------------------- /backend/session/cookies.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gorilla/sessions" 6 | "go.uber.org/zap" 7 | "net/http" 8 | ) 9 | 10 | const UserKey = "user" 11 | 12 | type Cookies struct { 13 | config Config 14 | store *sessions.CookieStore 15 | logger *zap.Logger 16 | } 17 | 18 | type Config interface { 19 | GetWebSecretKey() string 20 | } 21 | 22 | func New(config Config, logger *zap.Logger) *Cookies { 23 | return &Cookies{ 24 | config: config, 25 | logger: logger, 26 | } 27 | } 28 | 29 | func (c *Cookies) Start() error { 30 | c.Reset() 31 | return nil 32 | } 33 | 34 | func (c *Cookies) Reset() { 35 | c.store = sessions.NewCookieStore([]byte(c.config.GetWebSecretKey())) 36 | } 37 | 38 | func (c *Cookies) getSession(r *http.Request) (*sessions.Session, error) { 39 | return c.store.Get(r, "session") 40 | } 41 | 42 | func (c *Cookies) SetSessionUser(w http.ResponseWriter, r *http.Request, user string) error { 43 | session, err := c.getSession(r) 44 | if err != nil { 45 | c.logger.Error("cannot update session", zap.Error(err)) 46 | return err 47 | } 48 | session.Values[UserKey] = user 49 | return session.Save(r, w) 50 | } 51 | 52 | func (c *Cookies) ClearSessionUser(w http.ResponseWriter, r *http.Request) error { 53 | r.Header.Del("Cookie") 54 | session, err := c.getSession(r) 55 | if err != nil { 56 | return err 57 | } 58 | delete(session.Values, UserKey) 59 | return session.Save(r, w) 60 | } 61 | 62 | func (c *Cookies) GetSessionUser(r *http.Request) (string, error) { 63 | session, err := c.getSession(r) 64 | if err != nil { 65 | return "", err 66 | } 67 | user, found := session.Values[UserKey] 68 | if !found { 69 | return "", fmt.Errorf("no session found") 70 | } 71 | 72 | return user.(string), nil 73 | } 74 | -------------------------------------------------------------------------------- /backend/snap/changes_client.go: -------------------------------------------------------------------------------- 1 | package snap 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/syncloud/platform/snap/model" 7 | "go.uber.org/zap" 8 | ) 9 | 10 | type ChangesClient struct { 11 | logger *zap.Logger 12 | client ChangesHttpClient 13 | } 14 | 15 | type ChangesHttpClient interface { 16 | Get(url string) ([]byte, error) 17 | } 18 | 19 | func NewChangesClient(client ChangesHttpClient, logger *zap.Logger) *ChangesClient { 20 | return &ChangesClient{ 21 | client: client, 22 | logger: logger, 23 | } 24 | } 25 | 26 | func (s *ChangesClient) Changes() (*model.InstallerStatus, error) { 27 | s.logger.Info("snap changes") 28 | result := &model.InstallerStatus{IsRunning: false, Progress: make(map[string]model.InstallerProgress)} 29 | 30 | bodyBytes, err := s.client.Get("http://unix/v2/changes?select=in-progress") 31 | if err != nil { 32 | return nil, err 33 | } 34 | var response model.ServerResponse 35 | err = json.Unmarshal(bodyBytes, &response) 36 | if err != nil { 37 | s.logger.Error("cannot unmarshal", zap.Error(err)) 38 | return nil, err 39 | } 40 | if response.Status != "OK" { 41 | var errorResponse model.ServerError 42 | err = json.Unmarshal(response.Result, &errorResponse) 43 | if err != nil { 44 | s.logger.Error("cannot unmarshal", zap.Error(err)) 45 | return nil, err 46 | } 47 | 48 | return nil, fmt.Errorf(errorResponse.Message) 49 | } 50 | 51 | var changesResponse []model.Change 52 | err = json.Unmarshal(response.Result, &changesResponse) 53 | if err != nil { 54 | s.logger.Error("cannot unmarshal", zap.Error(err)) 55 | return nil, err 56 | } 57 | 58 | for _, change := range changesResponse { 59 | progress := change.InstallerProgress() 60 | result.Progress[progress.App] = progress 61 | result.IsRunning = true 62 | } 63 | return result, nil 64 | } 65 | -------------------------------------------------------------------------------- /backend/snap/cli.go: -------------------------------------------------------------------------------- 1 | package snap 2 | 3 | import ( 4 | "github.com/syncloud/platform/cli" 5 | "github.com/syncloud/platform/snap/model" 6 | "go.uber.org/zap" 7 | ) 8 | 9 | type Cli struct { 10 | executor cli.Executor 11 | logger *zap.Logger 12 | } 13 | 14 | func NewCli(executor cli.Executor, logger *zap.Logger) *Cli { 15 | return &Cli{ 16 | executor: executor, 17 | logger: logger, 18 | } 19 | } 20 | 21 | func (s *Cli) Start(name string) error { 22 | return s.run("start", name) 23 | } 24 | 25 | func (s *Cli) Stop(name string) error { 26 | return s.run("stop", name) 27 | } 28 | 29 | func (s *Cli) Run(name string) error { 30 | return s.run("run", name) 31 | } 32 | 33 | func (s *Cli) RunCmdIfExists(snap model.Snap, name string) error { 34 | cmd := snap.FindCommand(name) 35 | if cmd != nil { 36 | err := s.Run(cmd.FullName()) 37 | if err != nil { 38 | return err 39 | } 40 | } 41 | return nil 42 | } 43 | 44 | func (s *Cli) run(command string, name string) error { 45 | _, err := s.executor.CombinedOutput("snap", command, name) 46 | if err != nil { 47 | s.logger.Error("snap failed", zap.String("command", command), zap.Error(err)) 48 | return err 49 | } 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /backend/snap/cli_test.go: -------------------------------------------------------------------------------- 1 | package snap 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "github.com/syncloud/platform/log" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | type ExecutorStub struct { 12 | executions []string 13 | } 14 | 15 | func (e *ExecutorStub) CombinedOutput(name string, arg ...string) ([]byte, error) { 16 | e.executions = append(e.executions, fmt.Sprintf("%s %s", name, strings.Join(arg, " "))) 17 | return make([]byte, 0), nil 18 | } 19 | 20 | func TestStart(t *testing.T) { 21 | executor := &ExecutorStub{} 22 | service := NewCli(executor, log.Default()) 23 | err := service.Start("service1") 24 | assert.Nil(t, err) 25 | assert.Len(t, executor.executions, 1) 26 | assert.Equal(t, "snap start service1", executor.executions[0]) 27 | } 28 | 29 | func TestStop(t *testing.T) { 30 | executor := &ExecutorStub{} 31 | service := NewCli(executor, log.Default()) 32 | err := service.Stop("service1") 33 | assert.Nil(t, err) 34 | assert.Len(t, executor.executions, 1) 35 | assert.Equal(t, "snap stop service1", executor.executions[0]) 36 | } 37 | -------------------------------------------------------------------------------- /backend/snap/client.go: -------------------------------------------------------------------------------- 1 | package snap 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "go.uber.org/zap" 7 | "io" 8 | "net" 9 | "net/http" 10 | ) 11 | 12 | var NotFound = errors.New("app not found") 13 | 14 | type SnapdHttpClient struct { 15 | client HttpClient 16 | logger *zap.Logger 17 | } 18 | 19 | type HttpClient interface { 20 | Get(url string) (resp *http.Response, err error) 21 | Post(url, bodyType string, body io.Reader) (*http.Response, error) 22 | } 23 | 24 | func NewSnapdHttpClient(logger *zap.Logger) *SnapdHttpClient { 25 | return &SnapdHttpClient{ 26 | client: &http.Client{ 27 | Transport: &http.Transport{ 28 | DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { 29 | return net.Dial("unix", SOCKET) 30 | }, 31 | }, 32 | }, 33 | logger: logger, 34 | } 35 | } 36 | 37 | func (c *SnapdHttpClient) Get(url string) ([]byte, error) { 38 | resp, err := c.client.Get(url) 39 | if err != nil { 40 | c.logger.Error("cannot connect", zap.Error(err)) 41 | return nil, err 42 | } 43 | defer resp.Body.Close() 44 | if resp.StatusCode == http.StatusNotFound { 45 | return nil, NotFound 46 | } 47 | bodyBytes, err := io.ReadAll(resp.Body) 48 | if err != nil { 49 | c.logger.Error("cannot read output", zap.Error(err)) 50 | return nil, err 51 | } 52 | return bodyBytes, nil 53 | } 54 | 55 | func (c *SnapdHttpClient) Post(url, bodyType string, body io.Reader) (*http.Response, error) { 56 | return c.client.Post(url, bodyType, body) 57 | } 58 | -------------------------------------------------------------------------------- /backend/snap/client_test.go: -------------------------------------------------------------------------------- 1 | package snap 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | type HttpClientStub struct { 13 | response string 14 | status int 15 | err error 16 | } 17 | 18 | func (c *HttpClientStub) Get(_ string) (*http.Response, error) { 19 | r := io.NopCloser(bytes.NewReader([]byte(c.response))) 20 | return &http.Response{ 21 | StatusCode: c.status, 22 | Body: r, 23 | }, c.err 24 | } 25 | 26 | func (c *HttpClientStub) Post(_, _ string, _ io.Reader) (*http.Response, error) { 27 | //TODO implement me 28 | panic("implement me") 29 | } 30 | 31 | func TestSnapdHttpClient_Get_404_IsError(t *testing.T) { 32 | json := ` 33 | { 34 | "type":"error", 35 | "status-code":404, 36 | "status":"Not Found", 37 | "result":{ 38 | "message":"snap not installed", 39 | "kind":"snap-not-found", 40 | "value":"files" 41 | } 42 | } 43 | ` 44 | snapd := &SnapdHttpClient{ 45 | client: &HttpClientStub{response: json, status: 404}, 46 | } 47 | _, err := snapd.Get("url") 48 | assert.NotNil(t, err) 49 | assert.ErrorIs(t, err, NotFound) 50 | } 51 | 52 | func TestSnapdHttpClient_Get_500_IsNotError(t *testing.T) { 53 | json := ` 54 | { 55 | "type": "sync", 56 | "status-code": 200, 57 | "status": "OK", 58 | "result": [ 59 | { 60 | "id": "123" 61 | } 62 | ] 63 | } 64 | ` 65 | snapd := &SnapdHttpClient{ 66 | client: &HttpClientStub{response: json, status: 500}, 67 | } 68 | resp, err := snapd.Get("url") 69 | assert.Nil(t, err) 70 | assert.Equal(t, json, string(resp)) 71 | } 72 | -------------------------------------------------------------------------------- /backend/snap/model/app.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type App struct { 8 | Name string `json:"name"` 9 | Snap string `json:"snap"` 10 | } 11 | 12 | func (app *App) FullName() string { 13 | return fmt.Sprintf("%v.%v", app.Snap, app.Name) 14 | } 15 | -------------------------------------------------------------------------------- /backend/snap/model/change.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "regexp" 4 | 5 | type Change struct { 6 | Id string `json:"id"` 7 | Summary string `json:"summary"` 8 | Tasks []Task `json:"tasks"` 9 | } 10 | 11 | func (c Change) InstallerProgress() InstallerProgress { 12 | app := ParseApp(c.Summary) 13 | for _, task := range c.Tasks { 14 | if task.Status == "Doing" { 15 | if task.Kind == "download-snap" { 16 | return InstallerProgress{ 17 | App: app, 18 | Summary: "Downloading", 19 | Indeterminate: false, 20 | Percentage: CalcPercentage(task.Progress.Done, task.Progress.Total), 21 | } 22 | } 23 | } 24 | } 25 | return InstallerProgress{ 26 | App: app, 27 | Summary: ParseAction(c.Summary), 28 | Indeterminate: true, 29 | } 30 | } 31 | 32 | func CalcPercentage(done, total int64) int64 { 33 | if total <= 1 { 34 | return 0 35 | } 36 | return done * 100 / total 37 | } 38 | 39 | func ParseApp(summary string) string { 40 | r := regexp.MustCompile(`^.*? "(.*?)" .*`) 41 | match := r.FindStringSubmatch(summary) 42 | if match != nil { 43 | return match[1] 44 | } 45 | return "unknown" 46 | } 47 | 48 | func ParseAction(summary string) string { 49 | r := regexp.MustCompile(`^(.*?) .*`) 50 | match := r.FindStringSubmatch(summary) 51 | if match != nil { 52 | switch match[1] { 53 | case "Refresh": 54 | return "Upgrading" 55 | case "Install": 56 | return "Installing" 57 | case "Remove": 58 | return "Removing" 59 | } 60 | } 61 | return "Unknown" 62 | } 63 | 64 | type Task struct { 65 | Kind string `json:"kind"` 66 | Status string `json:"status"` 67 | Summary string `json:"summary"` 68 | Progress Progress `json:"progress"` 69 | } 70 | 71 | type Progress struct { 72 | Done int64 `json:"done"` 73 | Total int64 `json:"total"` 74 | } 75 | -------------------------------------------------------------------------------- /backend/snap/model/change_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestParseApp(t *testing.T) { 9 | assert.Equal(t, "matrix", ParseApp(`Refresh "matrix" snap from "latest/stable" channel`)) 10 | assert.Equal(t, "matrix", ParseApp(`"Install "matrix" snap from "latest/stable" channel`)) 11 | assert.Equal(t, "matrix", ParseApp(`"Remove "matrix" snap from "latest/stable" channel`)) 12 | assert.Equal(t, "unknown", ParseApp(`"doing something`)) 13 | } 14 | 15 | func TestParseAction(t *testing.T) { 16 | assert.Equal(t, "Upgrading", ParseAction(`Refresh "matrix" snap from "latest/stable" channel`)) 17 | assert.Equal(t, "Installing", ParseAction(`Install "matrix" snap from "latest/stable" channel`)) 18 | assert.Equal(t, "Removing", ParseAction(`Remove "matrix" snap from "latest/stable" channel`)) 19 | assert.Equal(t, "Unknown", ParseAction(`doing something`)) 20 | } 21 | 22 | func TestCalcPercentage(t *testing.T) { 23 | assert.Equal(t, int64(0), CalcPercentage(0, 0)) 24 | assert.Equal(t, int64(0), CalcPercentage(1, 1)) 25 | assert.Equal(t, int64(9), CalcPercentage(12, 123)) 26 | } 27 | -------------------------------------------------------------------------------- /backend/snap/model/installe_request.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type InstallRequest struct { 4 | Action string `json:"action"` 5 | } 6 | -------------------------------------------------------------------------------- /backend/snap/model/installer_info.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type InstallerInfo struct { 4 | StoreVersion string `json:"store_version"` 5 | InstalledVersion string `json:"installed_version"` 6 | } 7 | -------------------------------------------------------------------------------- /backend/snap/model/installer_status_response.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type InstallerStatus struct { 4 | IsRunning bool `json:"is_running"` 5 | Progress map[string]InstallerProgress `json:"progress"` 6 | } 7 | 8 | type InstallerProgress struct { 9 | App string `json:"app"` 10 | Summary string `json:"summary"` 11 | Indeterminate bool `json:"indeterminate"` 12 | Percentage int64 `json:"percentage"` 13 | } 14 | -------------------------------------------------------------------------------- /backend/snap/model/server_error.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type ServerError struct { 4 | Message string `json:"message"` 5 | } 6 | -------------------------------------------------------------------------------- /backend/snap/model/server_response.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "encoding/json" 4 | 5 | type ServerResponse struct { 6 | Result json.RawMessage `json:"result"` 7 | Status string `json:"status"` 8 | } 9 | -------------------------------------------------------------------------------- /backend/snap/model/snap.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Snap struct { 9 | Name string `json:"name"` 10 | Summary string `json:"summary"` 11 | Channel string `json:"channel"` 12 | Version string `json:"version"` 13 | Type string `json:"type"` 14 | Apps []App `json:"apps"` 15 | } 16 | 17 | func (s *Snap) ToStoreApp(url string) SyncloudAppVersions { 18 | app := s.toSyncloudApp(url) 19 | app.CurrentVersion = &s.Version 20 | return app 21 | } 22 | 23 | func (s *Snap) ToInstalledApp(url string) SyncloudAppVersions { 24 | app := s.toSyncloudApp(url) 25 | app.InstalledVersion = &s.Version 26 | return app 27 | } 28 | 29 | func (s *Snap) toSyncloudApp(url string) SyncloudAppVersions { 30 | icon := strings.TrimPrefix(s.Channel, "latest/") 31 | return SyncloudAppVersions{ 32 | App: SyncloudApp{ 33 | Id: s.Name, 34 | Name: s.Summary, 35 | Url: url, 36 | Icon: fmt.Sprintf("/rest/proxy/image?channel=%s&app=%s", icon, s.Name), 37 | }, 38 | } 39 | } 40 | 41 | func (s *Snap) IsApp() bool { 42 | return s.Type == "app" 43 | } 44 | 45 | func (s *Snap) FindCommand(name string) *App { 46 | for _, snapApp := range s.Apps { 47 | if snapApp.Name == name { 48 | return &snapApp 49 | } 50 | } 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /backend/snap/model/snap_response.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type SnapResponse struct { 4 | Result Snap `json:"result"` 5 | Status string `json:"status"` 6 | } 7 | -------------------------------------------------------------------------------- /backend/snap/model/snap_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestToSyncloudApp_IconUrl(t *testing.T) { 9 | snap := &Snap{Name: "Test", Summary: "Summary", Channel: "stable", Version: "1", Type: "app", Apps: nil} 10 | app := snap.toSyncloudApp("url") 11 | assert.Equal(t, "/rest/proxy/image?channel=stable&app=Test", app.App.Icon) 12 | } 13 | 14 | func TestToSyncloudApp_NormalizeIconUrl_AfterLocalAmendInstall(t *testing.T) { 15 | snap := &Snap{Name: "Test", Summary: "Summary", Channel: "latest/stable", Version: "1", Type: "app", Apps: nil} 16 | app := snap.toSyncloudApp("url") 17 | assert.Equal(t, "/rest/proxy/image?channel=stable&app=Test", app.App.Icon) 18 | } 19 | -------------------------------------------------------------------------------- /backend/snap/model/snaps_response.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type SnapsResponse struct { 4 | Result []Snap `json:"result"` 5 | Status string `json:"status"` 6 | } 7 | -------------------------------------------------------------------------------- /backend/snap/model/syncloud_app.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type SyncloudApp struct { 4 | Id string `json:"id"` 5 | Name string `json:"name"` 6 | Required string `json:"required"` 7 | Ui string `json:"ui"` 8 | Url string `json:"url"` 9 | Icon string `json:"icon"` 10 | Description string `json:"description"` 11 | } 12 | -------------------------------------------------------------------------------- /backend/snap/model/syncloud_app_versions.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type SyncloudAppVersions struct { 4 | App SyncloudApp `json:"app"` 5 | CurrentVersion *string `json:"current_version"` 6 | InstalledVersion *string `json:"installed_version"` 7 | } 8 | -------------------------------------------------------------------------------- /backend/snap/model/system_info.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type SystemInfo struct { 4 | Result Result `json:"result"` 5 | } 6 | 7 | type Result struct { 8 | Version string `json:"version"` 9 | } 10 | -------------------------------------------------------------------------------- /backend/storage/btrfs/device_stats.go: -------------------------------------------------------------------------------- 1 | package btrfs 2 | 3 | type DeviceStats struct { 4 | Header struct { 5 | Version string `json:"version"` 6 | } `json:"__header"` 7 | DeviceStats []struct { 8 | Device string `json:"device"` 9 | Devid string `json:"devid"` 10 | WriteIoErrs string `json:"write_io_errs"` 11 | ReadIoErrs string `json:"read_io_errs"` 12 | FlushIoErrs string `json:"flush_io_errs"` 13 | CorruptionErrs string `json:"corruption_errs"` 14 | GenerationErrs string `json:"generation_errs"` 15 | } `json:"device-stats"` 16 | } 17 | 18 | func (d *DeviceStats) HasErrors(device string) bool { 19 | for _, stats := range d.DeviceStats { 20 | if stats.Device == device { 21 | return stats.WriteIoErrs != "0" || stats.ReadIoErrs != "0" || stats.FlushIoErrs != "0" || stats.CorruptionErrs != "0" || stats.GenerationErrs != "0" 22 | } 23 | } 24 | return false 25 | } 26 | -------------------------------------------------------------------------------- /backend/storage/btrfs/stats.go: -------------------------------------------------------------------------------- 1 | package btrfs 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/prometheus/procfs/btrfs" 6 | "github.com/syncloud/platform/cli" 7 | ) 8 | 9 | type Stats struct { 10 | config Config 11 | executor cli.Executor 12 | } 13 | 14 | func NewStats(config Config, executor cli.Executor) *Stats { 15 | return &Stats{ 16 | config: config, 17 | executor: executor, 18 | } 19 | } 20 | 21 | func (s *Stats) Info() ([]*btrfs.Stats, error) { 22 | fs, err := btrfs.NewDefaultFS() 23 | if err != nil { 24 | return nil, err 25 | } 26 | return fs.Stats() 27 | } 28 | 29 | func (s *Stats) RaidMode(uuid string) (string, error) { 30 | stats, err := s.Info() 31 | if err != nil { 32 | return "", err 33 | } 34 | 35 | for _, fs := range stats { 36 | if fs.UUID == uuid { 37 | for raid := range fs.Allocation.Data.Layouts { 38 | return raid, nil 39 | } 40 | } 41 | } 42 | return "", nil 43 | } 44 | 45 | func (s *Stats) HasErrors(device string) (bool, error) { 46 | output, err := s.executor.CombinedOutput(BTRFS, "--format", "json", "device", "stats", s.config.ExternalDiskDir()) 47 | if err != nil { 48 | return false, err 49 | } 50 | 51 | var result DeviceStats 52 | if err := json.Unmarshal(output, &result); err != nil { 53 | return false, err 54 | } 55 | return result.HasErrors(device), nil 56 | } 57 | -------------------------------------------------------------------------------- /backend/storage/btrfs/stats_test.go: -------------------------------------------------------------------------------- 1 | package btrfs 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | type StatsConfigStub struct { 9 | } 10 | 11 | func (c *StatsConfigStub) ExternalDiskDir() string { 12 | return "/mnt" 13 | } 14 | 15 | type StatsExecutorStub struct { 16 | output string 17 | } 18 | 19 | func (e *StatsExecutorStub) CombinedOutput(_ string, _ ...string) ([]byte, error) { 20 | return []byte(e.output), nil 21 | } 22 | 23 | func Test_HasErrors(t *testing.T) { 24 | executor := &StatsExecutorStub{output: ` 25 | { 26 | "__header": { 27 | "version": "1" 28 | }, 29 | "device-stats": [ 30 | { 31 | "device": "/dev/loop0", 32 | "devid": "1", 33 | "write_io_errs": "0", 34 | "read_io_errs": "0", 35 | "flush_io_errs": "0", 36 | "corruption_errs": "0", 37 | "generation_errs": "1" 38 | }, 39 | { 40 | "device": "/dev/loop1", 41 | "devid": "2", 42 | "write_io_errs": "0", 43 | "read_io_errs": "0", 44 | "flush_io_errs": "0", 45 | "corruption_errs": "0", 46 | "generation_errs": "0" 47 | } 48 | ] 49 | } 50 | `} 51 | stats := NewStats(&StatsConfigStub{}, executor) 52 | 53 | loop0Errors, err := stats.HasErrors("/dev/loop0") 54 | assert.Nil(t, err) 55 | assert.True(t, loop0Errors) 56 | 57 | loop1Errors, err := stats.HasErrors("/dev/loop1") 58 | assert.Nil(t, err) 59 | assert.False(t, loop1Errors) 60 | } 61 | -------------------------------------------------------------------------------- /backend/storage/free_space_checker.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/syncloud/platform/cli" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | type FreeSpaceChecker struct { 10 | executor cli.Executor 11 | } 12 | 13 | func NewFreeSpaceChecker(executor cli.Executor) *FreeSpaceChecker { 14 | return &FreeSpaceChecker{ 15 | executor: executor, 16 | } 17 | 18 | } 19 | 20 | func (f *FreeSpaceChecker) HasFreeSpace(device string) (bool, error) { 21 | output, err := f.executor.CombinedOutput("parted", device, "unit", "%", "print", "free", "--script", "--machine") 22 | if err != nil { 23 | return false, err 24 | } 25 | 26 | lines := strings.Split(strings.TrimSpace(string(output)), "\n") 27 | last := lines[len(lines)-1] 28 | if !strings.Contains(last, "free") { 29 | return false, nil 30 | } 31 | freeString := strings.Split(last, ":")[3] 32 | freeString = strings.TrimSuffix(freeString, "%") 33 | free, err := strconv.ParseFloat(freeString, 64) 34 | if err != nil { 35 | return false, err 36 | } 37 | 38 | return free > ExtendableFreePercent, nil 39 | } 40 | -------------------------------------------------------------------------------- /backend/storage/linker.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "errors" 5 | "go.uber.org/zap" 6 | "os" 7 | ) 8 | 9 | type Linker struct { 10 | logger *zap.Logger 11 | } 12 | 13 | func NewLinker(logger *zap.Logger) *Linker { 14 | return &Linker{ 15 | logger: logger, 16 | } 17 | } 18 | func (d *Linker) RelinkDisk(link string, target string) error { 19 | d.logger.Info("relink disk") 20 | err := os.Chmod(target, 0o755) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | fi, err := os.Lstat(link) 26 | if err != nil { 27 | if !errors.Is(err, os.ErrNotExist) { 28 | d.logger.Error("stat", zap.Error(err)) 29 | return err 30 | } 31 | } else { 32 | if fi.Mode()&os.ModeSymlink == os.ModeSymlink { 33 | err = os.Remove(link) 34 | if err != nil { 35 | return err 36 | } 37 | } 38 | } 39 | 40 | err = os.Symlink(target, link) 41 | return err 42 | } 43 | -------------------------------------------------------------------------------- /backend/storage/model/disk.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Disk struct { 9 | Name string `json:"name"` 10 | Device string `json:"device"` 11 | Size string `json:"size"` 12 | Partitions []Partition `json:"partitions"` 13 | Active bool `json:"active"` 14 | Uuid string `json:"uuid"` 15 | MountPoint string `json:"mount_point"` 16 | Raid string `json:"raid"` 17 | HasErrors bool `json:"has_errors"` 18 | Boot bool `json:"boot"` 19 | } 20 | 21 | type UiDeviceEntry struct { 22 | Name string `json:"name"` 23 | Device string `json:"device"` 24 | Size string `json:"size"` 25 | Active bool `json:"active"` 26 | } 27 | 28 | func NewDisk(name string, device string, size string, active bool, uuid string, mountPoint string, boot bool, partitions []Partition) *Disk { 29 | if name == "" { 30 | name = fmt.Sprintf("Disk %s", strings.TrimPrefix(device, "/dev/")) 31 | } 32 | return &Disk{ 33 | Name: name, 34 | Device: device, 35 | Size: size, 36 | Partitions: partitions, 37 | Active: active, 38 | Uuid: uuid, 39 | MountPoint: mountPoint, 40 | Boot: boot, 41 | } 42 | } 43 | 44 | func (d *Disk) HasRootPartition() bool { 45 | return d.FindRootPartition() != nil 46 | } 47 | 48 | func (d *Disk) IsAvailable() bool { 49 | if d.HasRootPartition() { 50 | return false 51 | } 52 | if d.Boot { 53 | return false 54 | } 55 | return true 56 | } 57 | 58 | func (d *Disk) AddPartition(partition Partition) { 59 | d.Partitions = append(d.Partitions, partition) 60 | } 61 | 62 | func (d *Disk) FindRootPartition() *Partition { 63 | for _, v := range d.Partitions { 64 | if v.isRootFs() { 65 | return &v 66 | } 67 | } 68 | return nil 69 | } 70 | 71 | func (d *Disk) String() string { 72 | var partitionStrings []string 73 | for _, v := range d.Partitions { 74 | v.ToString() 75 | } 76 | return fmt.Sprintf("%s: %s", d.Name, partitionStrings) 77 | } 78 | -------------------------------------------------------------------------------- /backend/storage/model/disk_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestFindRootPartitionSome(t *testing.T) { 9 | disk := Disk{"disk", "/dev/sda", "20", []Partition{ 10 | {"10", "/dev/sda1", "/", true, "ext4", false}, 11 | {"10", "/dev/sda2", "", true, "ext4", false}, 12 | }, true, "", "", "", false, false} 13 | 14 | assert.Equal(t, disk.FindRootPartition().Device, "/dev/sda1") 15 | } 16 | 17 | func TestFindRootPartition_Nil(t *testing.T) { 18 | disk := Disk{"disk", "/dev/sda", "20", []Partition{ 19 | {"10", "/dev/sda1", "/my", true, "ext4", false}, 20 | {"10", "/dev/sda2", "", true, "ext4", false}, 21 | }, true, "", "", "", false, false} 22 | assert.Nil(t, disk.FindRootPartition()) 23 | } 24 | -------------------------------------------------------------------------------- /backend/storage/model/lsblk_entry.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "golang.org/x/exp/slices" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | const PartTypeExtended = "0x5" 10 | 11 | var SupportedDeviceTypes []string 12 | 13 | func init() { 14 | SupportedDeviceTypes = []string{"disk", "loop"} 15 | } 16 | 17 | type LsblkEntry struct { 18 | Name string 19 | Size string 20 | DeviceType string 21 | MountPoint string 22 | PartType string 23 | FsType string 24 | Model string 25 | Active bool 26 | Uuid string 27 | Boot bool 28 | } 29 | 30 | func (e *LsblkEntry) IsExtendedPartition() bool { 31 | return e.PartType == PartTypeExtended 32 | } 33 | 34 | func (e *LsblkEntry) IsSupportedType() bool { 35 | if e.IsMMCBootPartition() { 36 | return false 37 | } 38 | if slices.Contains(SupportedDeviceTypes, e.DeviceType) { 39 | return true 40 | } 41 | if e.IsRaid() { 42 | return true 43 | } 44 | return false 45 | } 46 | 47 | func (e *LsblkEntry) IsMMCBootPartition() bool { 48 | return e.DetectMMCBootDevice() != nil 49 | } 50 | 51 | func (e *LsblkEntry) DetectMMCBootDevice() *string { 52 | r := regexp.MustCompile(`^(/dev/mmcblk\d+)boot\d+$`) 53 | match := r.FindStringSubmatch(e.Name) 54 | if match != nil { 55 | return &match[1] 56 | } 57 | return nil 58 | } 59 | 60 | func (e *LsblkEntry) IsSupportedFsType() bool { 61 | if e.FsType == "squashfs" { 62 | return false 63 | } 64 | if strings.HasPrefix(e.MountPoint, "/snap") { 65 | return false 66 | } 67 | if e.FsType == "linux_raid_member" { 68 | return false 69 | } 70 | return true 71 | } 72 | 73 | func (e *LsblkEntry) IsRaid() bool { 74 | if strings.HasPrefix(e.DeviceType, "raid") { 75 | return true 76 | } 77 | return false 78 | } 79 | 80 | func (e *LsblkEntry) ParentDevice() (string, error) { 81 | r, err := regexp.Compile(`(.*?)p?\d*$`) 82 | if err != nil { 83 | return "", err 84 | } 85 | 86 | match := r.FindStringSubmatch(e.Name) 87 | return match[1], nil 88 | } 89 | 90 | func (e *LsblkEntry) GetFsType() string { 91 | if e.IsRaid() { 92 | return "raid" 93 | } 94 | return e.FsType 95 | } 96 | -------------------------------------------------------------------------------- /backend/storage/model/lsblk_entry_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestLsblkEntry_IsMMCBootPartition(t *testing.T) { 9 | assert.True(t, (&LsblkEntry{Name: "/dev/mmcblk1boot0"}).IsMMCBootPartition()) 10 | } 11 | 12 | func TestLsblkEntry_IsMMCBootPartition_Not(t *testing.T) { 13 | assert.False(t, (&LsblkEntry{Name: "/dev/mmcblk1p0"}).IsMMCBootPartition()) 14 | } 15 | -------------------------------------------------------------------------------- /backend/storage/model/partition.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "golang.org/x/exp/slices" 6 | ) 7 | 8 | var NoPartitionFsTypes []string 9 | 10 | func init() { 11 | NoPartitionFsTypes = []string{"vfat", "exfat"} 12 | } 13 | 14 | type Partition struct { 15 | Size string `json:"size"` 16 | Device string `json:"device"` 17 | MountPoint string `json:"mount_point"` 18 | Active bool `json:"active"` 19 | FsType string `json:"fs_type"` 20 | Extendable bool `json:"extendable"` 21 | } 22 | 23 | func (p *Partition) PermissionsSupport() bool { 24 | return !slices.Contains(NoPartitionFsTypes, p.FsType) 25 | } 26 | 27 | func (p *Partition) isRootFs() bool { 28 | return p.MountPoint == "/" 29 | } 30 | 31 | func (p *Partition) ToString() string { 32 | return fmt.Sprintf("%s, %s, %s, %t", p.Device, p.Size, p.MountPoint, p.Active) 33 | } 34 | -------------------------------------------------------------------------------- /backend/storage/path_checker.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/syncloud/platform/config" 5 | "go.uber.org/zap" 6 | "path/filepath" 7 | ) 8 | 9 | type PathChecker struct { 10 | config *config.SystemConfig 11 | logger *zap.Logger 12 | } 13 | 14 | type Checker interface { 15 | ExternalDiskLinkExists() bool 16 | } 17 | 18 | func NewPathChecker(config *config.SystemConfig, logger *zap.Logger) *PathChecker { 19 | return &PathChecker{ 20 | config: config, 21 | logger: logger, 22 | } 23 | } 24 | 25 | func (c *PathChecker) ExternalDiskLinkExists() bool { 26 | realLinkPath, err := filepath.EvalSymlinks(c.config.DiskLink()) 27 | if err != nil { 28 | c.logger.Warn("cannot read disk link", zap.String("name", c.config.DiskLink())) 29 | return false 30 | } 31 | c.logger.Info("real link", zap.String("path", realLinkPath)) 32 | 33 | externalDiskPath := c.config.ExternalDiskDir() 34 | c.logger.Info("external disk", zap.String("path", externalDiskPath)) 35 | 36 | linkExists := realLinkPath == externalDiskPath 37 | c.logger.Info("link", zap.Bool("exists", linkExists)) 38 | 39 | return linkExists 40 | } 41 | -------------------------------------------------------------------------------- /backend/support/aggregator.go: -------------------------------------------------------------------------------- 1 | package support 2 | 3 | import ( 4 | "fmt" 5 | "go.uber.org/zap" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | ) 10 | 11 | const ( 12 | Pattern = "/var/snap/*/common/log/*.log" 13 | Separator = "\n----------------------\n" 14 | ) 15 | 16 | type LogAggregator struct { 17 | logger *zap.Logger 18 | } 19 | 20 | func NewAggregator(logger *zap.Logger) *LogAggregator { 21 | return &LogAggregator{ 22 | logger: logger, 23 | } 24 | } 25 | 26 | func (a *LogAggregator) GetLogs() string { 27 | log := a.fileLogs() 28 | log += a.cmd("date") 29 | log += a.cmd("mount") 30 | log += a.cmd("systemctl", "status", "--state=failed", "snap.*") 31 | log += a.cmd("top", "-n", "1", "-bc") 32 | log += a.cmd("ping", "google.com", "-c", "5") 33 | log += a.cmd("uname", "-a") 34 | log += a.cmd("cat", "/etc/debian_version") 35 | log += a.cmd("df", "-h") 36 | log += a.cmd("lsblk", "-o", "+UUID") 37 | log += a.cmd("lsblk", "-Pp", "-o", "NAME,SIZE,TYPE,MOUNTPOINT,PARTTYPE,FSTYPE,MODEL") 38 | log += a.cmd("ls", "-la", "/data") 39 | log += a.cmd("uptime") 40 | log += a.cmd("snap", "run", "platform.cli", "ipv4", "public") 41 | log += a.cmd("journalctl", "-n", "1000", "--no-pager") 42 | log += a.cmd("dmesg") 43 | return log 44 | } 45 | 46 | func (a *LogAggregator) cmd(app string, args ...string) string { 47 | command := exec.Command(app, args...) 48 | if app == "top" { 49 | command.Env = append(os.Environ(), "COLUMNS=1000") 50 | } 51 | result := command.String() + "\n\n" 52 | output, err := command.CombinedOutput() 53 | if err != nil { 54 | a.logger.Info(string(output)) 55 | a.logger.Warn("failed", zap.Error(err)) 56 | } 57 | 58 | result += string(output) + Separator 59 | return result 60 | } 61 | 62 | func (a *LogAggregator) fileLogs() string { 63 | 64 | matches, err := filepath.Glob(Pattern) 65 | if err != nil { 66 | a.logger.Error("failed", zap.Error(err)) 67 | return "" 68 | } 69 | 70 | log := "" 71 | for _, file := range matches { 72 | log += fmt.Sprintf("file: %s\n\n", file) 73 | output, err := exec.Command("tail", "-100", file).CombinedOutput() 74 | if err != nil { 75 | a.logger.Error("failed", zap.Error(err)) 76 | return "" 77 | } 78 | log += string(output) 79 | log += Separator 80 | } 81 | return log 82 | } 83 | -------------------------------------------------------------------------------- /backend/support/aggregator_test.go: -------------------------------------------------------------------------------- 1 | package support 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "github.com/syncloud/platform/log" 6 | "testing" 7 | ) 8 | 9 | func TestLogAggregator_GetLogs(t *testing.T) { 10 | aggregator := NewAggregator(log.Default()) 11 | logs := aggregator.GetLogs() 12 | assert.NotEmpty(t, logs) 13 | } 14 | -------------------------------------------------------------------------------- /backend/support/sender.go: -------------------------------------------------------------------------------- 1 | package support 2 | 3 | type Sender struct { 4 | aggregator *LogAggregator 5 | redirect Redirect 6 | } 7 | 8 | type Redirect interface { 9 | SendLogs(logs string, includeSupport bool) error 10 | } 11 | 12 | func NewSender(aggregator *LogAggregator, redirect Redirect) *Sender { 13 | return &Sender{ 14 | aggregator: aggregator, 15 | redirect: redirect, 16 | } 17 | } 18 | 19 | func (s *Sender) Send(includeSupport bool) error { 20 | logs := s.aggregator.GetLogs() 21 | return s.redirect.SendLogs(logs, includeSupport) 22 | } 23 | -------------------------------------------------------------------------------- /backend/systemd/journal.go: -------------------------------------------------------------------------------- 1 | package systemd 2 | 3 | import ( 4 | "github.com/syncloud/platform/cli" 5 | "strings" 6 | ) 7 | 8 | type Journal struct { 9 | executor cli.Executor 10 | } 11 | 12 | func NewJournal(executor cli.Executor) *Journal { 13 | return &Journal{ 14 | executor: executor, 15 | } 16 | } 17 | 18 | func (c *Journal) read(predicate func(string) bool, args ...string) []string { 19 | 20 | args = append(args, "-n", "1000", "--no-pager") 21 | output, err := c.executor.CombinedOutput("journalctl", args...) 22 | if err != nil { 23 | return []string{err.Error()} 24 | } 25 | var logs []string 26 | rawLogs := strings.Split(string(output), "\n") 27 | for _, line := range rawLogs { 28 | if predicate(line) { 29 | logs = append(logs, line) 30 | } 31 | } 32 | last := len(logs) - 1 33 | for i := 0; i < len(logs)/2; i++ { 34 | logs[i], logs[last-i] = logs[last-i], logs[i] 35 | } 36 | return logs 37 | } 38 | 39 | func (c *Journal) ReadAll(predicate func(string) bool) []string { 40 | return c.read(predicate) 41 | } 42 | 43 | func (c *Journal) ReadBackend(predicate func(string) bool) []string { 44 | return c.read(predicate, "-u", "snap.platform.backend") 45 | } 46 | -------------------------------------------------------------------------------- /backend/systemd/journal_test.go: -------------------------------------------------------------------------------- 1 | package systemd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | type JournalCtlExecutorStub struct { 11 | output string 12 | } 13 | 14 | func (e *JournalCtlExecutorStub) CombinedOutput(_ string, _ ...string) ([]byte, error) { 15 | return []byte(e.output), nil 16 | } 17 | 18 | func Test_ReadBackend_Filter(t *testing.T) { 19 | 20 | journalCtl := &JournalCtlExecutorStub{ 21 | output: ` 22 | 3 log {"category": "cat1"} 23 | 2 log 24 | 1 log {"category": "cat1", "key": "value"} 25 | `, 26 | } 27 | reader := NewJournal(journalCtl) 28 | logs := reader.ReadBackend(func(line string) bool { 29 | return strings.Contains(line, fmt.Sprintf(`"%s": "%s"`, "category", "cat1")) 30 | }) 31 | assert.Equal(t, []string{ 32 | `1 log {"category": "cat1", "key": "value"}`, 33 | `3 log {"category": "cat1"}`, 34 | }, logs) 35 | } 36 | 37 | func Test_ReadBackend_PercentChar(t *testing.T) { 38 | 39 | journalCtl := &JournalCtlExecutorStub{ 40 | output: `%1#2`, 41 | } 42 | reader := NewJournal(journalCtl) 43 | logs := reader.ReadBackend(func(line string) bool { 44 | return true 45 | }) 46 | assert.Equal(t, []string{ 47 | `%1#2`, 48 | }, logs) 49 | } 50 | -------------------------------------------------------------------------------- /backend/systemd/mount.go: -------------------------------------------------------------------------------- 1 | package systemd 2 | 3 | type Mount struct { 4 | What string 5 | Where string 6 | } 7 | -------------------------------------------------------------------------------- /backend/version/meta.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | ) 7 | 8 | type PlatformVersion struct{} 9 | 10 | type Version interface { 11 | Get() (string, error) 12 | } 13 | 14 | func New() *PlatformVersion { 15 | return &PlatformVersion{} 16 | } 17 | 18 | func (v *PlatformVersion) Get() (string, error) { 19 | content, err := os.ReadFile("/snap/platform/current/meta/version") 20 | if err != nil { 21 | return "", err 22 | } 23 | return strings.TrimSpace(string(content)), nil 24 | 25 | } 26 | -------------------------------------------------------------------------------- /bin/boot_extend.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | 3 | BOOT_PARTITION_INFO=$(lsblk -pP -o PKNAME,NAME,MOUNTPOINT | grep 'MOUNTPOINT="/"') 4 | DEVICE=$(echo ${BOOT_PARTITION_INFO} | cut -d' ' -f1 | cut -d'=' -f2 | tr -d '"') 5 | PARTITION=$(echo ${BOOT_PARTITION_INFO} | cut -d' ' -f2 | cut -d'=' -f2 | tr -d '"') 6 | PARTITION_NUM=2 7 | 8 | DEVICE_SIZE_BYTES=$(parted -sm ${DEVICE} unit B print | grep "^${DEVICE}:" | cut -d':' -f2 | cut -d'B' -f1) 9 | PART_START_BYTES=$(parted -sm ${DEVICE} unit B print | grep "^${PARTITION_NUM}:" | cut -d':' -f2 | cut -d'B' -f1) 10 | PART_END_BYTES=$(parted -sm ${DEVICE} unit B print | grep "^${PARTITION_NUM}:" | cut -d':' -f3 | cut -d'B' -f1) 11 | PART_START_SECTORS=$(expr ${PART_START_BYTES} / 512) 12 | PART_END_SECTORS=$(expr ${DEVICE_SIZE_BYTES} / 512 - 1) 13 | UNUSED_BYTES=$(( $DEVICE_SIZE_BYTES - $PART_END_BYTES )) 14 | MIN_FREE_SPACE_LIMIT_BYTES=100000 15 | if [[ $UNUSED_BYTES -lt $MIN_FREE_SPACE_LIMIT_BYTES ]]; then 16 | echo "unused space is: ${UNUSED_BYTES}b is less then min free space limit (${MIN_FREE_SPACE_LIMIT_BYTES}b), not extending" 17 | exit 0 18 | fi 19 | 20 | if parted -sm ${DEVICE} unit B print | grep "^3:"; then 21 | echo "3 or more partitions are not supported" 22 | exit 0 23 | fi 24 | 25 | if parted -sm ${DEVICE} unit B print | grep "btrfs"; then 26 | echo "btrfs not supported" 27 | exit 0 28 | fi 29 | 30 | PTTYPE=$(fdisk -l ${DEVICE} | grep "Disklabel type:" | awk '{ print $3 }') 31 | if [[ $PTTYPE == "gpt" ]]; then 32 | GPT_BACKUP_HEADER_SIZE=33 33 | PART_END_SECTORS=$(expr ${PART_END_SECTORS} - ${GPT_BACKUP_HEADER_SIZE}) 34 | 35 | echo " 36 | p 37 | d 38 | ${PARTITION_NUM} 39 | p 40 | n 41 | ${PARTITION_NUM} 42 | ${PART_START_SECTORS} 43 | ${PART_END_SECTORS} 44 | p 45 | w 46 | " | fdisk ${DEVICE} 47 | 48 | else 49 | 50 | echo " 51 | p 52 | d 53 | ${PARTITION_NUM} 54 | p 55 | n 56 | p 57 | ${PARTITION_NUM} 58 | ${PART_START_SECTORS} 59 | ${PART_END_SECTORS} 60 | p 61 | w 62 | q 63 | " | fdisk ${DEVICE} 64 | 65 | fi 66 | 67 | partprobe 68 | 69 | resize2fs ${PARTITION} 70 | -------------------------------------------------------------------------------- /bin/copy_to_emmc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | SOURCE_DEVICE_NAME="/dev/mmcblk0" 4 | TARGET_DEVICE_NAME="/dev/mmcblk1" 5 | 6 | SOURCE_DEVICE_SIZE=$(blockdev --getsize64 ${SOURCE_DEVICE_NAME}) 7 | TARGET_DEVICE_SIZE=$(blockdev --getsize64 ${TARGET_DEVICE_NAME}) 8 | BYTES_TO_COPY=${SOURCE_DEVICE_SIZE} 9 | 10 | echo "source device ($SOURCE_DEVICE_NAME) size: ${SOURCE_DEVICE_SIZE} bytes" 11 | echo "target device ($TARGET_DEVICE_NAME) size: ${TARGET_DEVICE_SIZE} bytes" 12 | 13 | if [[ ${TARGET_DEVICE_SIZE} -lt ${SOURCE_DEVICE_SIZE} ]]; then 14 | echo "target device size is less then target device" 15 | if [[ $1 == "-f" ]]; then 16 | BYTES_TO_COPY=${TARGET_DEVICE_SIZE} 17 | else 18 | echo "use -f to copy only bytes to fit on target (dangerous)" 19 | exit 1 20 | fi 21 | fi 22 | 23 | KIBI_BYTE=1024 24 | KIBI_BYTES_TO_COPY=$(($BYTES_TO_COPY/$KIBI_BYTE)) 25 | 26 | dd if=${SOURCE_DEVICE_NAME} of=${TARGET_DEVICE_NAME} bs=${KIBI_BYTE} count=${KIBI_BYTES_TO_COPY} -------------------------------------------------------------------------------- /bin/cpu_frequency: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | date > /var/log/cpu_frequency.log 3 | echo "checking if cpu frequency has to be changed" >> /var/log/cpu_frequency.log 4 | if [ -f /var/lib/cpu_frequency_control ]; then 5 | echo "changing cpu frequency" >> /var/log/cpu_frequency.log 6 | for cpu in $(ls /sys/devices/system/cpu/ | grep 'cpu[0-9][0-9]*');do 7 | cat /var/lib/cpu_frequency_governor > /sys/devices/system/cpu/${cpu}/cpufreq/scaling_governor 8 | cat /var/lib/cpu_frequency_max > /sys/devices/system/cpu/${cpu}/cpufreq/scaling_max_freq 9 | cat /var/lib/cpu_frequency_min > /sys/devices/system/cpu/${cpu}/cpufreq/scaling_min_freq 10 | done 11 | else 12 | echo "not changing cpu frequency" >> /var/log/cpu_frequency.log 13 | fi -------------------------------------------------------------------------------- /bin/disk_format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd ) 4 | 5 | if [[ -z "$1" ]]; then 6 | echo "usage $0 device" 7 | exit 1 8 | fi 9 | 10 | DEVICE=$1 11 | PARTITION=1 12 | 13 | dd if=/dev/zero of=${DEVICE} bs=512 count=1 conv=notrunc 14 | export LD_LIBRARY_PATH=${DIR}/gptfdisk/lib 15 | ${DIR}/gptfdisk/bin/sgdisk -o ${DEVICE} 16 | ${DIR}/gptfdisk/bin/sgdisk -n ${PARTITION} ${DEVICE} 17 | ${DIR}/gptfdisk/bin/sgdisk -p ${DEVICE} 18 | PARTITION_DEVICE=$(lsblk -pl -o NAME,TYPE ${DEVICE} | grep part | awk '{print $1}') 19 | mkfs.ext4 -F ${PARTITION_DEVICE} -------------------------------------------------------------------------------- /bin/service.api.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 4 | 5 | exec $DIR/api unix ${SNAP_COMMON}/api.socket 6 | -------------------------------------------------------------------------------- /bin/service.authelia.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd ) 4 | rm -rf /var/snap/platform/current/authelia.socket 5 | exec ${DIR}/authelia/authelia.sh \ 6 | --config /var/snap/platform/current/config/authelia/config.yml \ 7 | --config.experimental.filters template 8 | -------------------------------------------------------------------------------- /bin/service.backend.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 4 | 5 | exec $DIR/backend unix ${SNAP_DATA}/backend.sock 6 | -------------------------------------------------------------------------------- /bin/service.nginx-public.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd ) 4 | 5 | if [[ -z "$1" ]]; then 6 | echo "usage $0 [start|stop]" 7 | exit 1 8 | fi 9 | 10 | case $1 in 11 | start) 12 | ${DIR}/nginx/bin/nginx.sh -t -c /var/snap/platform/current/nginx.conf -e stderr 13 | exec $DIR/nginx/bin/nginx.sh -c /var/snap/platform/current/nginx.conf -e stderr 14 | ;; 15 | reload) 16 | $DIR/nginx/bin/nginx.sh -c /var/snap/platform/current/nginx.conf -s reload -e stderr 17 | ;; 18 | *) 19 | echo "not valid command" 20 | exit 1 21 | ;; 22 | esac 23 | 24 | -------------------------------------------------------------------------------- /bin/service.openldap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd ) 5 | 6 | export LD_LIBRARY_PATH=$DIR/openldap/lib 7 | export SASL_PATH=$DIR/openldap/lib 8 | export SASL_CONF_PATH=$DIR/openldap/lib 9 | 10 | SOCKET="${SNAP_DATA}/openldap.socket" 11 | exec ${DIR}/openldap/sbin/slapd.sh -h "ldap://127.0.0.1:389 ldapi://${SOCKET//\//%2F}" -F /var/snap/platform/current/slapd.d 12 | -------------------------------------------------------------------------------- /bin/update_certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | cp /snap/platform/current/certs/* /usr/share/ca-certificates/mozilla/ 4 | /usr/sbin/update-ca-certificates -------------------------------------------------------------------------------- /bin/upgrade-snapd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | 3 | VERSION=$(curl http://apps.syncloud.org/releases/stable/snapd2.version) 4 | ARCH=$(dpkg --print-architecture) 5 | SNAPD=snapd-${VERSION}-${ARCH}.tar.gz 6 | 7 | cd /tmp 8 | rm -rf "${SNAPD}" 9 | rm -rf snapd 10 | wget http://apps.syncloud.org/apps/"${SNAPD}" --progress=dot:giga 11 | tar xzvf "${SNAPD}" 12 | ./snapd/upgrade.sh 13 | -------------------------------------------------------------------------------- /config/.gitignore: -------------------------------------------------------------------------------- 1 | ports.cfg 2 | dns_token.cfg -------------------------------------------------------------------------------- /config/authelia/authrequest.conf: -------------------------------------------------------------------------------- 1 | ## Send a subrequest to Authelia to verify if the user is authenticated and has permission to access the resource. 2 | auth_request /api/verify; 3 | 4 | ## Set the $target_url variable based on the original request. 5 | 6 | ## Comment this line if you're using nginx without the http_set_misc module. 7 | #set_escape_uri $target_url $scheme://$http_host$request_uri; 8 | 9 | ## Uncomment this line if you're using NGINX without the http_set_misc module. 10 | set $target_url https://$http_host$request_uri; 11 | 12 | ## Save the upstream response headers from Authelia to variables. 13 | auth_request_set $user $upstream_http_remote_user; 14 | auth_request_set $groups $upstream_http_remote_groups; 15 | auth_request_set $name $upstream_http_remote_name; 16 | auth_request_set $email $upstream_http_remote_email; 17 | 18 | ## Inject the response headers from the variables into the request made to the backend. 19 | proxy_set_header Remote-User $user; 20 | proxy_set_header Remote-Groups $groups; 21 | proxy_set_header Remote-Name $name; 22 | proxy_set_header Remote-Email $email; 23 | 24 | ## If the subreqest returns 200 pass to the backend, if the subrequest returns 401 redirect to the portal. 25 | error_page 401 =302 {{ .Domain }}?rd=$target_url; 26 | -------------------------------------------------------------------------------- /config/authelia/location.conf: -------------------------------------------------------------------------------- 1 | location /authelia/api/verify { 2 | ## Essential Proxy Configuration 3 | internal; 4 | proxy_pass http://authelia; 5 | 6 | ## Headers 7 | ## The headers starting with X-* are required. 8 | proxy_set_header X-Original-URL https://$http_host$request_uri; 9 | proxy_set_header X-Original-Method $request_method; 10 | proxy_set_header X-Forwarded-Method $request_method; 11 | proxy_set_header X-Forwarded-Proto https; 12 | proxy_set_header X-Forwarded-Host $http_host; 13 | proxy_set_header X-Forwarded-Uri $request_uri; 14 | proxy_set_header X-Forwarded-For $remote_addr; 15 | proxy_set_header Content-Length ""; 16 | proxy_set_header Connection ""; 17 | 18 | ## Basic Proxy Configuration 19 | proxy_pass_request_body off; 20 | proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; # Timeout if the real server is dead 21 | # proxy_redirect http:// $scheme://; 22 | proxy_http_version 1.1; 23 | proxy_cache_bypass $cookie_session; 24 | proxy_no_cache $cookie_session; 25 | proxy_buffers 4 32k; 26 | client_body_buffer_size 128k; 27 | 28 | ## Advanced Proxy Configuration 29 | send_timeout 5m; 30 | proxy_read_timeout 240; 31 | proxy_send_timeout 240; 32 | proxy_connect_timeout 240; 33 | } 34 | 35 | location /authelia { 36 | include /var/snap/platform/current/config/authelia/proxy.conf; 37 | 38 | proxy_pass http://authelia; 39 | 40 | } 41 | -------------------------------------------------------------------------------- /config/authelia/proxy.conf: -------------------------------------------------------------------------------- 1 | ## Headers 2 | proxy_set_header Host $host; 3 | proxy_set_header X-Original-URL https://$http_host$request_uri; 4 | proxy_set_header X-Forwarded-Proto https; 5 | proxy_set_header X-Forwarded-Host $http_host; 6 | proxy_set_header X-Forwarded-Uri $request_uri; 7 | proxy_set_header X-Forwarded-Ssl on; 8 | proxy_set_header X-Forwarded-For $remote_addr; -------------------------------------------------------------------------------- /config/authelia/users.yaml: -------------------------------------------------------------------------------- 1 | users: 2 | fake: 3 | disabled: true 4 | displayname: 'Fake' 5 | #password: 'syncloud' 6 | password: '$argon2id$v=19$m=65536,t=3,p=4$THP1NKRQoN1ebBtEQhxpLA$2HyzgO/a8o6VVXB0DnNi9jbxlDWE8J0ctyRNTCVGSYE' 7 | email: 'fake' 8 | groups: [] 9 | 10 | 11 | -------------------------------------------------------------------------------- /config/ldap/init.ldif: -------------------------------------------------------------------------------- 1 | dn: dc=syncloud,dc=org 2 | objectClass: dcObject 3 | objectClass: organizationalUnit 4 | ou: syncloud 5 | 6 | # administrator 7 | dn: cn=admin,dc=syncloud,dc=org 8 | objectClass: simpleSecurityObject 9 | objectClass: organizationalRole 10 | cn: admin 11 | description: Administrator 12 | userPassword: syncloud 13 | 14 | # Subtree for Users 15 | dn: ou=users,dc=syncloud,dc=org 16 | ou: Users 17 | description: Users 18 | objectClass: organizationalUnit 19 | objectClass: top 20 | 21 | # administrator 22 | dn: cn=${user},ou=users,dc=syncloud,dc=org 23 | objectClass: simpleSecurityObject 24 | objectClass: Person 25 | objectClass: inetOrgPerson 26 | objectClass: posixAccount 27 | uidNumber: 10 28 | gidNumber: 10 29 | homeDirectory: ${user} 30 | uid: ${user} 31 | cn: ${name} 32 | sn: ${user} 33 | displayName: ${user} 34 | description: ${user} 35 | userPassword: ${password} 36 | mail: ${email} 37 | 38 | 39 | # Subtree for Groups 40 | dn: ou=groups,dc=syncloud,dc=org 41 | ou: Groups 42 | description: Groups 43 | objectClass: organizationalUnit 44 | objectClass: top 45 | 46 | # Admin group 47 | dn: cn=syncloud,ou=groups,dc=syncloud,dc=org 48 | objectClass: posixGroup 49 | objectClass: top 50 | gidNumber: 1 51 | cn: syncloud 52 | description: Syncloud 53 | memberUid: ${user} 54 | 55 | dn: ou=Policies,dc=syncloud,dc=org 56 | ou: Policies 57 | objectClass: organizationalUnit 58 | 59 | dn: cn=passwordDefault,ou=Policies,dc=syncloud,dc=org 60 | objectClass: pwdPolicy 61 | objectClass: person 62 | objectClass: top 63 | cn: passwordDefault 64 | sn: passwordDefault 65 | pwdAttribute: userPassword 66 | pwdAllowUserChange: TRUE 67 | 68 | -------------------------------------------------------------------------------- /config/ldap/upgrade/cn=module{0}.ldif: -------------------------------------------------------------------------------- 1 | # AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify. 2 | # CRC32 d0a533a6 3 | dn: cn=module{0} 4 | objectClass: olcModuleList 5 | cn: module{0} 6 | olcModulePath: /snap/platform/current/openldap/usr/lib/ldap 7 | olcModuleLoad: {0}memberof.la 8 | olcModuleLoad: {1}back_mdb.la 9 | olcModuleLoad: {2}back_ldap.la 10 | olcModuleLoad: {3}ppolicy.la 11 | structuralObjectClass: olcModuleList 12 | entryUUID: 8eb35784-a457-103b-95ce-a72b865042ff 13 | creatorsName: cn=config 14 | createTimestamp: 20210907184546Z 15 | entryCSN: 20210907184546.415671Z#000000#000#000000 16 | modifiersName: cn=config 17 | modifyTimestamp: 20210907184546Z -------------------------------------------------------------------------------- /config/mount/mount.template: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=External disk 3 | Before=local-fs.target 4 | 5 | [Mount] 6 | What={{.What}} 7 | Where={{.Where}} 8 | 9 | [Install] 10 | WantedBy=local-fs.target 11 | -------------------------------------------------------------------------------- /config/platform.cfg: -------------------------------------------------------------------------------- 1 | [platform] 2 | apps_root: /snap 3 | hooks_root: %(apps_root)s 4 | data_root: /var/snap 5 | configs_root: /var/snap 6 | config_root: /var/snap/platform/common 7 | app_dir: /snap/platform/current 8 | data_dir: /var/snap/platform/current 9 | common_dir: /var/snap/platform/common 10 | config_dir: %(app_dir)s/config 11 | bin_dir: %(app_dir)s/bin 12 | www_root_public: %(app_dir)s/www 13 | nginx: %(app_dir)s/nginx/sbin/nginx 14 | log_root: %(common_dir)s/log 15 | user_config: %(common_dir)s/user_platform.cfg 16 | disk_root: /opt/disk 17 | internal_disk_dir: %(disk_root)s/internal 18 | external_disk_dir: %(disk_root)s/external 19 | disk_link: /data 20 | ssh_port: 22 21 | platform_log: %(common_dir)s/log/platform.log 22 | rest_internal_log: %(common_dir)s/log/rest_internal.log 23 | rest_public_log: %(common_dir)s/log/rest_public.log 24 | ssl_key_file: %(data_dir)s/syncloud.key 25 | ssl_certificate_file: %(data_dir)s/syncloud.crt 26 | ssl_ca_key_file: %(data_dir)s/syncloud.ca.key 27 | ssl_ca_certificate_file: %(data_dir)s/syncloud.ca.crt 28 | channel: stable 29 | -------------------------------------------------------------------------------- /meta/snap.yaml: -------------------------------------------------------------------------------- 1 | apps: 2 | nginx-public: 3 | command: bin/service.nginx-public.sh start 4 | daemon: forking 5 | plugs: 6 | - network 7 | - network-bind 8 | restart-condition: always 9 | reload-command: bin/service.nginx-public.sh reload 10 | start-timeout: 2000s 11 | openldap: 12 | command: bin/service.openldap.sh 13 | daemon: forking 14 | plugs: 15 | - network 16 | - network-bind 17 | restart-condition: always 18 | start-timeout: 2000s 19 | backend: 20 | command: bin/service.backend.sh 21 | daemon: simple 22 | plugs: 23 | - network 24 | - network-bind 25 | start-timeout: 2000s 26 | restart-condition: always 27 | api: 28 | command: bin/service.api.sh 29 | daemon: simple 30 | plugs: 31 | - network 32 | - network-bind 33 | start-timeout: 2000s 34 | restart-condition: always 35 | authelia: 36 | command: bin/service.authelia.sh 37 | daemon: simple 38 | plugs: 39 | - network 40 | - network-bind 41 | restart-condition: always 42 | start-timeout: 2000s 43 | 44 | cli: 45 | command: bin/cli 46 | btrfs: 47 | command: btrfs/bin/btrfs.sh 48 | mkfs-btrfs: 49 | command: btrfs/bin/mkfs.sh 50 | authelia-cli: 51 | command: authelia/authelia.sh 52 | 53 | confinement: strict 54 | description: Syncloud Platform 55 | grade: stable 56 | type: base 57 | name: platform 58 | summary: Syncloud Platform 59 | -------------------------------------------------------------------------------- /nginx/bin/nginx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd ) 3 | LIBS=$(echo ${DIR}/lib/*-linux-gnu*) 4 | LIBS=$LIBS:$(echo ${DIR}/usr/lib/*-linux-gnu*) 5 | ${DIR}/lib/*-linux*/ld-*.so --library-path $LIBS ${DIR}/usr/sbin/nginx "$@" 6 | -------------------------------------------------------------------------------- /nginx/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ex 2 | 3 | DIR=$( cd "$( dirname "$0" )" && pwd ) 4 | cd ${DIR} 5 | 6 | BUILD_DIR=${DIR}/../build/snap/nginx 7 | mkdir -p ${BUILD_DIR} 8 | cp -r /etc ${BUILD_DIR} 9 | cp -r /opt ${BUILD_DIR} 10 | cp -r /usr ${BUILD_DIR} 11 | cp -r /bin ${BUILD_DIR} 12 | cp -r /lib ${BUILD_DIR} 13 | cp -r ${DIR}/bin/* ${BUILD_DIR}/bin 14 | -------------------------------------------------------------------------------- /nginx/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ex 2 | 3 | DIR=$( cd "$( dirname "$0" )" && pwd ) 4 | cd ${DIR} 5 | 6 | BUILD_DIR=${DIR}/../build/snap/nginx 7 | ${BUILD_DIR}/bin/nginx.sh -version 8 | -------------------------------------------------------------------------------- /package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | 3 | DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 4 | 5 | if [[ -z "$1" ]]; then 6 | echo "usage $0 version" 7 | exit 1 8 | fi 9 | 10 | NAME=platform 11 | ARCH=$(uname -m) 12 | VERSION=$1 13 | CA_CERTIFICATES_VERSION=20250419 14 | cd ${DIR}/build 15 | 16 | BUILD_DIR=${DIR}/build/snap 17 | 18 | apt update 19 | apt install -y wget squashfs-tools dpkg-dev 20 | 21 | cp -r ${DIR}/bin ${BUILD_DIR} 22 | cp -r ${DIR}/config ${BUILD_DIR} 23 | 24 | wget http://ftp.us.debian.org/debian/pool/main/c/ca-certificates/ca-certificates_${CA_CERTIFICATES_VERSION}_all.deb 25 | dpkg -x ca-certificates_${CA_CERTIFICATES_VERSION}_all.deb . 26 | mv usr/share/ca-certificates/mozilla ${BUILD_DIR}/certs 27 | 28 | wget --retry-on-http-error=503 --progress=dot:giga https://github.com/syncloud/3rdparty/releases/download/gptfdisk/gptfdisk-${ARCH}.tar.gz 29 | tar xf gptfdisk-${ARCH}.tar.gz 30 | mv gptfdisk ${BUILD_DIR} 31 | wget --retry-on-http-error=503 --progress=dot:giga https://github.com/syncloud/3rdparty/releases/download/openldap/openldap-${ARCH}.tar.gz 32 | tar xf openldap-${ARCH}.tar.gz 33 | mv openldap ${BUILD_DIR} 34 | wget --retry-on-http-error=503 --progress=dot:giga https://github.com/syncloud/3rdparty/releases/download/btrfs/btrfs-${ARCH}.tar.gz 35 | tar xf btrfs-${ARCH}.tar.gz 36 | mv btrfs ${BUILD_DIR} 37 | 38 | cd ${DIR}/build 39 | 40 | echo "snapping" 41 | ARCH=$(dpkg-architecture -q DEB_HOST_ARCH) 42 | 43 | cp -r ${DIR}/meta ${BUILD_DIR} 44 | echo ${VERSION} >> ${BUILD_DIR}/meta/version 45 | echo "version: $VERSION" >> ${BUILD_DIR}/meta/snap.yaml 46 | echo "architectures:" >> ${BUILD_DIR}/meta/snap.yaml 47 | echo "- ${ARCH}" >> ${BUILD_DIR}/meta/snap.yaml 48 | 49 | PACKAGE=${NAME}_${VERSION}_${ARCH}.snap 50 | echo ${PACKAGE} > $DIR/package.name 51 | mksquashfs ${BUILD_DIR} ${DIR}/${PACKAGE} -noappend -comp xz -no-xattrs -all-root 52 | mkdir ${DIR}/artifact 53 | cp ${DIR}/${PACKAGE} ${DIR}/artifact 54 | 55 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | log 3 | geckodriver 4 | screenshot 5 | firefox 6 | *.xpi 7 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syncloud/platform/6334691a9f66fe3fa92208fd9109bfe84ff8101b/test/__init__.py -------------------------------------------------------------------------------- /test/api/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/syncloud/platform/lib 2 | 3 | go 1.16 4 | 5 | require github.com/stretchr/testify v1.7.0 6 | -------------------------------------------------------------------------------- /test/api/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 7 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 11 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 12 | -------------------------------------------------------------------------------- /test/api/model.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | type Response struct { 4 | Success bool `json:"success"` 5 | Message string `json:"message,omitempty"` 6 | Data *string `json:"data,omitempty"` 7 | } 8 | -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- 1 | from os.path import dirname 2 | 3 | from syncloudlib.integration.conftest import * 4 | 5 | DIR = dirname(__file__) 6 | 7 | 8 | @pytest.fixture(scope="session") 9 | def project_dir(): 10 | return join(dirname(__file__), '..') 11 | 12 | 13 | @pytest.fixture(scope='session') 14 | def main_domain(): 15 | return 'redirect' 16 | 17 | 18 | @pytest.fixture(scope='session') 19 | def full_domain(domain, main_domain): 20 | return '{}.{}'.format(domain, main_domain) 21 | -------------------------------------------------------------------------------- /test/deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | apt-get update 4 | apt-get install -y sshpass openssh-client netcat rustc file libxml2-dev libxslt-dev build-essential libz-dev curl apache2-utils 5 | pip install -r requirements.txt 6 | -------------------------------------------------------------------------------- /test/id.cfg: -------------------------------------------------------------------------------- 1 | [id] 2 | name=test -------------------------------------------------------------------------------- /test/install-snapd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | 3 | VERSION=$(curl http://apps.syncloud.org/releases/stable/snapd2.version) 4 | ARCH=$(dpkg --print-architecture) 5 | SNAPD=snapd-${VERSION}-${ARCH}.tar.gz 6 | 7 | cd /tmp 8 | rm -rf "${SNAPD}" 9 | rm -rf snapd 10 | wget http://apps.syncloud.org/apps/"${SNAPD}" --progress=dot:giga 11 | tar xzvf "${SNAPD}" 12 | mkdir -p /var/lib/snapd/snaps 13 | ./snapd/install.sh 14 | -------------------------------------------------------------------------------- /test/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest==6.2.4 2 | responses==0.5.0 3 | selenium==3.141.0 4 | syncloud-lib==305 5 | -------------------------------------------------------------------------------- /test/test-upgrade.py: -------------------------------------------------------------------------------- 1 | from subprocess import run 2 | 3 | import pytest 4 | import requests 5 | from syncloudlib.http import wait_for_rest 6 | from syncloudlib.integration.hosts import add_host_alias 7 | from syncloudlib.integration.installer import local_install 8 | 9 | TMP_DIR = '/tmp/syncloud' 10 | 11 | 12 | @pytest.fixture(scope="session") 13 | def module_setup(request, device, artifact_dir): 14 | def module_teardown(): 15 | device.run_ssh('journalctl > {0}/upgrade.journalctl.log'.format(TMP_DIR), throw=False) 16 | device.scp_from_device('{0}/*'.format(TMP_DIR), artifact_dir) 17 | run('cp /videos/* {0}'.format(artifact_dir), shell=True) 18 | run('chmod -R a+r {0}'.format(artifact_dir), shell=True) 19 | 20 | request.addfinalizer(module_teardown) 21 | 22 | 23 | def test_start(module_setup, app, device_host, domain, device): 24 | add_host_alias(app, device_host, domain) 25 | device.activated() 26 | device.run_ssh('rm -rf {0}'.format(TMP_DIR), throw=False) 27 | device.run_ssh('mkdir {0}'.format(TMP_DIR), throw=False) 28 | 29 | 30 | def test_upgrade(device, device_user, device_password, device_host, app_archive_path, app_domain, app_dir): 31 | device.run_ssh('snap remove platform') 32 | device.run_ssh('/test/install-snapd.sh') 33 | device.run_ssh('snap install platform', retries=3) 34 | local_install(device_host, device_password, app_archive_path) 35 | wait_for_rest(requests.session(), "https://{0}".format(app_domain), 200, 10) 36 | -------------------------------------------------------------------------------- /test/test.desktop.ldif: -------------------------------------------------------------------------------- 1 | dn: cn=testdesktop,ou=users,dc=syncloud,dc=org 2 | objectClass: simpleSecurityObject 3 | objectClass: Person 4 | objectClass: inetOrgPerson 5 | objectClass: posixAccount 6 | uidNumber: 11 7 | gidNumber: 11 8 | homeDirectory: testuser 9 | uid: testuser 10 | cn: testuser 11 | sn: testuser 12 | displayName: testuser 13 | description: testuser 14 | userPassword: password 15 | mail: testuser@example.com 16 | -------------------------------------------------------------------------------- /test/test.mobile.ldif: -------------------------------------------------------------------------------- 1 | dn: cn=testmobile,ou=users,dc=syncloud,dc=org 2 | objectClass: simpleSecurityObject 3 | objectClass: Person 4 | objectClass: inetOrgPerson 5 | objectClass: posixAccount 6 | uidNumber: 11 7 | gidNumber: 11 8 | homeDirectory: testuser 9 | uid: testuser 10 | cn: testuser 11 | sn: testuser 12 | displayName: testuser 13 | description: testuser 14 | userPassword: password 15 | mail: testuser@example.com 16 | -------------------------------------------------------------------------------- /test/testapp/bin/access-change: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | snap run platform.cli config get platform.domain > /var/snap/testapp/common/on_access_change -------------------------------------------------------------------------------- /test/testapp/bin/service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | while true; do echo testapp.service is running; sleep 10; done -------------------------------------------------------------------------------- /test/testapp/bin/storage-change: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | echo "changed" > /var/snap/testapp/common/on_storage_change -------------------------------------------------------------------------------- /test/testapp/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | 3 | DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 4 | ARCH=$(uname -m) 5 | BUILD_DIR=${DIR}/build 6 | mkdir ${BUILD_DIR} 7 | 8 | ARCH=$(dpkg-architecture -q DEB_HOST_ARCH) 9 | cp -r ${DIR}/meta ${BUILD_DIR} 10 | cp -r ${DIR}/bin ${BUILD_DIR} 11 | echo "architectures:" >> ${BUILD_DIR}/meta/snap.yaml 12 | echo "- ${ARCH}" >> ${BUILD_DIR}/meta/snap.yaml 13 | 14 | mksquashfs ${BUILD_DIR} ${DIR}/testapp.snap -noappend -comp xz -no-xattrs -all-root 15 | cp ${DIR}/*.snap ${DIR}/../../artifact 16 | rm -rf ${BUILD_DIR} 17 | -------------------------------------------------------------------------------- /test/testapp/meta/hooks/install: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | /usr/sbin/useradd -r -s /bin/false testapp -------------------------------------------------------------------------------- /test/testapp/meta/snap.yaml: -------------------------------------------------------------------------------- 1 | apps: 2 | service: 3 | command: bin/service.sh 4 | daemon: simple 5 | start-timeout: 200s 6 | restart-condition: always 7 | 8 | storage-change: 9 | command: bin/storage-change 10 | access-change: 11 | command: bin/access-change 12 | 13 | confinement: strict 14 | description: Test app 15 | grade: stable 16 | name: testapp 17 | summary: Test app 18 | version: 1 19 | -------------------------------------------------------------------------------- /test/wait-ssh.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | attempts=100 4 | attempt=0 5 | CMD="sshpass -p syncloud ssh -o StrictHostKeyChecking=no root@$1" 6 | 7 | set +e 8 | ${CMD} date 9 | while test $? -gt 0 10 | do 11 | if [[ ${attempt} -gt ${attempts} ]]; then 12 | exit 1 13 | fi 14 | sleep 3 15 | echo "Waiting for SSH $attempt" 16 | attempt=$((attempt+1)) 17 | ${CMD} date 18 | done 19 | set -e 20 | -------------------------------------------------------------------------------- /wiki/images/gparted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syncloud/platform/6334691a9f66fe3fa92208fd9109bfe84ff8101b/wiki/images/gparted.png -------------------------------------------------------------------------------- /wiki/images/performance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syncloud/platform/6334691a9f66fe3fa92208fd9109bfe84ff8101b/wiki/images/performance.png -------------------------------------------------------------------------------- /www/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /www/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /www/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | 'plugin:vue/vue3-essential', 5 | '@vue/standard' 6 | ], 7 | rules: { 8 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 9 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 10 | 'vue/multi-word-component-names': 'off', 11 | }, 12 | overrides: [ 13 | { 14 | files: [ 15 | '**/__tests__/*.{j,t}s?(x)', 16 | '**/tests/unit/**/*.spec.{j,t}s?(x)' 17 | ], 18 | env: { 19 | jest: true 20 | } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /www/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /dist 3 | .env.local 4 | .env.*.local 5 | npm-debug.log* 6 | .idea 7 | coverage -------------------------------------------------------------------------------- /www/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by 'unplugin-auto-import' 2 | export {} 3 | declare global { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /www/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', 4 | { 5 | targets: { 6 | node: 'current' 7 | } 8 | } 9 | ] 10 | ] 11 | } -------------------------------------------------------------------------------- /www/components.d.ts: -------------------------------------------------------------------------------- 1 | // generated by unplugin-vue-components 2 | // We suggest you to commit this file into source control 3 | // Read more: https://github.com/vuejs/core/pull/3399 4 | import '@vue/runtime-core' 5 | 6 | export {} 7 | 8 | declare module '@vue/runtime-core' { 9 | export interface GlobalComponents { 10 | Dialog: typeof import('./src/components/Dialog.vue')['default'] 11 | ElButton: typeof import('element-plus/es')['ElButton'] 12 | ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] 13 | ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] 14 | ElCol: typeof import('element-plus/es')['ElCol'] 15 | ElDialog: typeof import('element-plus/es')['ElDialog'] 16 | ElInput: typeof import('element-plus/es')['ElInput'] 17 | ElOption: typeof import('element-plus/es')['ElOption'] 18 | ElProgress: typeof import('element-plus/es')['ElProgress'] 19 | ElRadio: typeof import('element-plus/es')['ElRadio'] 20 | ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] 21 | ElRow: typeof import('element-plus/es')['ElRow'] 22 | ElSelect: typeof import('element-plus/es')['ElSelect'] 23 | ElStep: typeof import('element-plus/es')['ElStep'] 24 | ElSteps: typeof import('element-plus/es')['ElSteps'] 25 | ElSwitch: typeof import('element-plus/es')['ElSwitch'] 26 | ElTable: typeof import('element-plus/es')['ElTable'] 27 | ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] 28 | Error: typeof import('./src/components/Error.vue')['default'] 29 | Menu: typeof import('./src/components/Menu.vue')['default'] 30 | Notification: typeof import('./src/components/Notification.vue')['default'] 31 | RouterLink: typeof import('vue-router')['RouterLink'] 32 | RouterView: typeof import('vue-router')['RouterView'] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Syncloud 9 | 10 | 11 | 12 | 13 | 14 | 17 |
18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /www/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | moduleFileExtensions: [ 4 | 'js', 5 | 'ts', 6 | 'json', 7 | 'vue' 8 | ], 9 | transform: { 10 | '^.+\\.ts$': 'ts-jest', 11 | '^.+\\.js$': 'babel-jest', 12 | '^.+\\.vue$': '@vue/vue3-jest' 13 | }, 14 | testEnvironment: 'jsdom', 15 | setupFiles: ['./tests/setup.js'], 16 | setupFilesAfterEnv: ['./tests/setup-after-env.js'], 17 | testEnvironmentOptions: { 18 | customExportConditions: ['node', 'node-addons'] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "syncloud", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "serve": "vite preview", 9 | "lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src", 10 | "test": "jest" 11 | }, 12 | "dependencies": { 13 | "axios": "^1.6.0", 14 | "bootstrap": "^3.3.5", 15 | "browserslist": "^4.21.9", 16 | "caniuse-lite": "^1.0.30001517", 17 | "element-plus": "^2.4.1", 18 | "font-awesome": "^4.7.0", 19 | "material-icons": "^1.12.0", 20 | "querystring": "^0.2.0", 21 | "roboto-fontface": "^0.10.0", 22 | "vue": "^3.2.40", 23 | "vue-router": "^4.0.0-0" 24 | }, 25 | "devDependencies": { 26 | "@babel/preset-env": "^7.19.3", 27 | "@rollup/plugin-inject": "^5.0.5", 28 | "@types/jest": "^29.1.1", 29 | "@vitejs/plugin-vue": "^1.6.1", 30 | "@vue/eslint-config-standard": "^5.1.2", 31 | "@vue/test-utils": "^2.2.3", 32 | "@vue/vue3-jest": "^29.1.1", 33 | "axios-mock-adapter": "^1.19.0", 34 | "babel-core": "^7.0.0-bridge.0", 35 | "babel-eslint": "^10.1.0", 36 | "babel-jest": "^29.1.2", 37 | "body-parser": "^1.19.0", 38 | "eslint": "^8.24.0", 39 | "eslint-plugin-import": "^2.20.2", 40 | "eslint-plugin-node": "^11.1.0", 41 | "eslint-plugin-promise": "^4.2.1", 42 | "eslint-plugin-standard": "^4.0.0", 43 | "eslint-plugin-vue": "^8.7.1", 44 | "flush-promises": "^1.0.2", 45 | "jest": "^29.1.2", 46 | "jest-environment-jsdom": "^29.1.2", 47 | "jsdom": "^20.0.1", 48 | "sass": "^1.26.5", 49 | "ts-jest": "^29.0.3", 50 | "typescript": "4.3.5", 51 | "unplugin-auto-import": "^0.11.2", 52 | "unplugin-vue-components": "^0.22.7", 53 | "vite": "^2.5.4", 54 | "miragejs": "^0.1.45" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /www/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syncloud/platform/6334691a9f66fe3fa92208fd9109bfe84ff8101b/www/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /www/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syncloud/platform/6334691a9f66fe3fa92208fd9109bfe84ff8101b/www/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /www/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syncloud/platform/6334691a9f66fe3fa92208fd9109bfe84ff8101b/www/public/apple-touch-icon.png -------------------------------------------------------------------------------- /www/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syncloud/platform/6334691a9f66fe3fa92208fd9109bfe84ff8101b/www/public/favicon-16x16.png -------------------------------------------------------------------------------- /www/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syncloud/platform/6334691a9f66fe3fa92208fd9109bfe84ff8101b/www/public/favicon-32x32.png -------------------------------------------------------------------------------- /www/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syncloud/platform/6334691a9f66fe3fa92208fd9109bfe84ff8101b/www/public/favicon.ico -------------------------------------------------------------------------------- /www/public/images/penguin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syncloud/platform/6334691a9f66fe3fa92208fd9109bfe84ff8101b/www/public/images/penguin.png -------------------------------------------------------------------------------- /www/public/images/wordpress-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syncloud/platform/6334691a9f66fe3fa92208fd9109bfe84ff8101b/www/public/images/wordpress-128.png -------------------------------------------------------------------------------- /www/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syncloud/platform/6334691a9f66fe3fa92208fd9109bfe84ff8101b/www/public/mstile-150x150.png -------------------------------------------------------------------------------- /www/public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 20 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /www/src/VueApp.vue: -------------------------------------------------------------------------------- 1 | 6 | 71 | 76 | -------------------------------------------------------------------------------- /www/src/assets/appcenter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | appcenter 6 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /www/src/assets/appcenterh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | appcenterh 6 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /www/src/assets/apps.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | apps 6 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /www/src/assets/appsh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | appsh 6 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /www/src/assets/email.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | email 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /www/src/assets/emailinput.svg: -------------------------------------------------------------------------------- 1 | emailinput -------------------------------------------------------------------------------- /www/src/assets/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | facebook 10 | 11 | 14 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /www/src/assets/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /www/src/assets/logout.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | logout 6 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /www/src/assets/logouth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | logouth 6 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /www/src/assets/nameinput.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | nameinput 6 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /www/src/assets/passinput.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | pasinput 6 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /www/src/assets/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | settnigs 6 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /www/src/assets/settingsh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | settingsh 6 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /www/src/assets/shutdown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | shutdown 6 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /www/src/assets/shutdownh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | shutdownh 6 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /www/src/components/Dialog.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 45 | -------------------------------------------------------------------------------- /www/src/components/Error.vue: -------------------------------------------------------------------------------- 1 | 19 | 76 | -------------------------------------------------------------------------------- /www/src/components/Notification.vue: -------------------------------------------------------------------------------- 1 | 43 | -------------------------------------------------------------------------------- /www/src/js/common.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export function checkForServiceError (data, onComplete, onError) { 4 | if ('success' in data && !data.success) { 5 | const err = { 6 | response: { 7 | status: 200, 8 | data: data 9 | } 10 | } 11 | onError(err) 12 | } else { 13 | onComplete() 14 | } 15 | } 16 | 17 | export const INSTALLER_STATUS_URL = '/rest/installer/status' 18 | export const DEFAULT_STATUS_PREDICATE = (response) => { 19 | return response.data.data.is_running 20 | } 21 | 22 | export const JOB_STATUS_URL = '/rest/job/status' 23 | export const JOB_STATUS_PREDICATE = (response) => { 24 | return response.data.data.status !== 'Idle' 25 | } 26 | 27 | export function runAfterJobIsComplete (timeoutFunc, onComplete, onError, statusUrl, statusPredicate) { 28 | const recheckFunc = function () { 29 | runAfterJobIsComplete(timeoutFunc, onComplete, onError, statusUrl, statusPredicate) 30 | } 31 | 32 | const recheckTimeout = 2000 33 | axios.get(statusUrl) 34 | .then(response => { 35 | if (statusPredicate(response)) { 36 | timeoutFunc(recheckFunc, recheckTimeout) 37 | } else { 38 | onComplete() 39 | } 40 | }) 41 | .catch(err => { 42 | console.log('status err: ' + err) 43 | // Auth error means job is finished 44 | if (err.response !== undefined && err.response.status === 401) { 45 | onComplete() 46 | } else { 47 | timeoutFunc(recheckFunc, recheckTimeout) 48 | } 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /www/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import VueApp from './VueApp.vue' 3 | import router from './router' 4 | import 'element-plus/dist/index.css' 5 | import { mock } from './stub/api' 6 | 7 | if (import.meta.env.DEV) { 8 | mock() 9 | } 10 | 11 | createApp(VueApp) 12 | .use(router) 13 | .mount('#app') 14 | -------------------------------------------------------------------------------- /www/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | 3 | const routes = [ 4 | { path: '/', name: 'Apps', component: () => import('../views/Apps.vue') }, 5 | { path: '/login', name: 'Login', component: () => import('../views/Login.vue') }, 6 | { path: '/app', name: 'App', component: () => import('../views/App.vue') }, 7 | { path: '/appcenter', name: 'AppCenter', component: () => import('../views/AppCenter.vue') }, 8 | { path: '/settings', name: 'Settings', component: () => import('../views/Settings.vue') }, 9 | { path: '/activation', name: 'Activation', component: () => import('../views/Activation.vue') }, 10 | { path: '/activate', name: 'Activate', component: () => import('../views/Activate.vue') }, 11 | { path: '/backup', name: 'Backup', component: () => import('../views/Backup.vue') }, 12 | { path: '/network', name: 'Network', component: () => import('../views/Network.vue') }, 13 | { path: '/access', name: 'Access', component: () => import('../views/Access.vue') }, 14 | { path: '/storage', name: 'Storage', component: () => import('../views/Storage.vue') }, 15 | { path: '/internalmemory', name: 'InternalMemory', component: () => import('../views/InternalMemory.vue') }, 16 | { path: '/updates', name: 'Updates', component: () => import('../views/Updates.vue') }, 17 | { path: '/support', name: 'Support', component: () => import('../views/Support.vue') }, 18 | { path: '/certificate', name: 'Certificate', component: () => import('../views/Certificate.vue') }, 19 | { path: '/certificate/log', name: 'Certificate Log', component: () => import('../views/CertificateLog.vue') }, 20 | { path: '/logs', name: 'Logs', component: () => import('../views/Logs.vue') }, 21 | { path: '/:catchAll(.*)', redirect: '/' } 22 | ] 23 | 24 | const router = createRouter({ 25 | history: createWebHistory(import.meta.env.BASE_URL), 26 | routes 27 | }) 28 | 29 | export default router 30 | -------------------------------------------------------------------------------- /www/src/views/AppCenter.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 73 | 77 | -------------------------------------------------------------------------------- /www/src/views/Apps.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 72 | 76 | -------------------------------------------------------------------------------- /www/src/views/CertificateLog.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 69 | 73 | -------------------------------------------------------------------------------- /www/src/views/Logs.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 69 | 73 | -------------------------------------------------------------------------------- /www/src/views/Network.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 50 | 54 | -------------------------------------------------------------------------------- /www/tests/setup-after-env.js: -------------------------------------------------------------------------------- 1 | jest.setTimeout(30000) 2 | -------------------------------------------------------------------------------- /www/tests/setup.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syncloud/platform/6334691a9f66fe3fa92208fd9109bfe84ff8101b/www/tests/setup.js -------------------------------------------------------------------------------- /www/tests/unit/Certificate.spec.js: -------------------------------------------------------------------------------- 1 | import { mount, RouterLinkStub } from '@vue/test-utils' 2 | import axios from 'axios' 3 | import MockAdapter from 'axios-mock-adapter' 4 | import flushPromises from 'flush-promises' 5 | import Certificate from '../../src/views/Certificate.vue' 6 | 7 | jest.setTimeout(30000) 8 | 9 | test('Certificate', async () => { 10 | const showError = jest.fn() 11 | const mockRouter = { push: jest.fn() } 12 | 13 | const mock = new MockAdapter(axios) 14 | mock.onGet('/rest/certificate').reply(200, 15 | { 16 | data: { 17 | is_valid: true, 18 | is_real: false, 19 | valid_for_days: 10 20 | }, 21 | success: true 22 | } 23 | ) 24 | const wrapper = mount(Certificate, 25 | { 26 | attachTo: document.body, 27 | global: { 28 | components: { 29 | RouterLink: RouterLinkStub 30 | }, 31 | stubs: { 32 | Error: { 33 | template: '', 34 | methods: { 35 | showAxios: showError 36 | } 37 | }, 38 | Dialog: true 39 | }, 40 | mocks: { 41 | $router: mockRouter 42 | } 43 | } 44 | } 45 | ) 46 | 47 | await flushPromises() 48 | 49 | await expect(wrapper.find('#valid_days').text()).toBe('10') 50 | 51 | expect(showError).toHaveBeenCalledTimes(0) 52 | wrapper.unmount() 53 | }) 54 | -------------------------------------------------------------------------------- /www/tests/unit/CertificateLog.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import axios from 'axios' 3 | import MockAdapter from 'axios-mock-adapter' 4 | import flushPromises from 'flush-promises' 5 | import CertificateLog from '../../src/views/CertificateLog.vue' 6 | 7 | jest.setTimeout(30000) 8 | 9 | test('Certificate logs', async () => { 10 | const showError = jest.fn() 11 | 12 | const mock = new MockAdapter(axios) 13 | mock.onGet('/rest/certificate/log').reply(200, 14 | { 15 | data: [ 16 | 'log 1', 17 | 'log 2' 18 | ], 19 | success: true 20 | } 21 | ) 22 | const wrapper = mount(CertificateLog, 23 | { 24 | attachTo: document.body, 25 | global: { 26 | stubs: { 27 | Error: { 28 | template: '', 29 | methods: { 30 | showAxios: showError 31 | } 32 | }, 33 | Dialog: true 34 | } 35 | } 36 | } 37 | ) 38 | 39 | await flushPromises() 40 | 41 | await expect(wrapper.find('#logs').text()).toBe('log 1log 2') 42 | 43 | expect(showError).toHaveBeenCalledTimes(0) 44 | wrapper.unmount() 45 | }) 46 | -------------------------------------------------------------------------------- /www/tests/unit/InternalMemory.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import axios from 'axios' 3 | import MockAdapter from 'axios-mock-adapter' 4 | import flushPromises from 'flush-promises' 5 | import InternalMemory from '../../src/views/InternalMemory.vue' 6 | 7 | jest.setTimeout(30000) 8 | 9 | test('Extend', async () => { 10 | const showError = jest.fn() 11 | let extended = false 12 | const mockRouter = { push: jest.fn() } 13 | 14 | const mock = new MockAdapter(axios) 15 | 16 | mock.onGet('/rest/storage/boot/disk').reply(function (_) { 17 | return [200, { 18 | data: { 19 | device: '/dev/mmcblk0p2', 20 | size: '2G', 21 | extendable: true 22 | }, 23 | success: true 24 | }] 25 | }) 26 | 27 | mock.onPost('/rest/storage/boot_extend').reply(function (_) { 28 | extended = true 29 | return [200, { success: true }] 30 | }) 31 | 32 | let statusCalled = false 33 | mock.onGet('/rest/job/status').reply(function (_) { 34 | statusCalled = true 35 | return [200, {success: true, data:{status: "Idle"}}] 36 | }) 37 | 38 | const wrapper = mount(InternalMemory, 39 | { 40 | attachTo: document.body, 41 | global: { 42 | stubs: { 43 | Error: { 44 | template: '', 45 | methods: { 46 | showAxios: showError 47 | } 48 | } 49 | }, 50 | mocks: { 51 | $route: { path: '/app', query: { id: 'files' } }, 52 | $router: mockRouter 53 | } 54 | } 55 | } 56 | ) 57 | 58 | await flushPromises() 59 | 60 | expect(showError).toHaveBeenCalledTimes(0) 61 | 62 | await wrapper.find('#btn_boot_extend').trigger('click') 63 | // await wrapper.find('#btn_confirm').trigger('click') 64 | 65 | await flushPromises() 66 | 67 | expect(showError).toHaveBeenCalledTimes(0) 68 | expect(extended).toBe(true) 69 | expect(statusCalled).toBeTruthy() 70 | wrapper.unmount() 71 | }) 72 | -------------------------------------------------------------------------------- /www/tests/unit/Login.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import axios from 'axios' 3 | import MockAdapter from 'axios-mock-adapter' 4 | import flushPromises from 'flush-promises' 5 | import Login from '../../src/views/Login.vue' 6 | import { ElButton } from 'element-plus' 7 | 8 | jest.setTimeout(30000) 9 | 10 | test('Login', async () => { 11 | const showError = jest.fn() 12 | const mockRouter = { push: jest.fn() } 13 | 14 | let username 15 | let password 16 | 17 | const mock = new MockAdapter(axios) 18 | mock.onPost('/rest/login').reply(function (config) { 19 | const request = JSON.parse(config.data) 20 | username = request.username 21 | password = request.password 22 | return [200, { success: true }] 23 | }) 24 | 25 | const wrapper = mount(Login, 26 | { 27 | attachTo: document.body, 28 | props: { 29 | checkUserSession: jest.fn(), 30 | activated: true 31 | }, 32 | global: { 33 | stubs: { 34 | Error: { 35 | template: '', 36 | methods: { 37 | showAxios: showError 38 | } 39 | }, 40 | 'el-button': ElButton 41 | }, 42 | mocks: { 43 | $route: { path: '/login' }, 44 | $router: mockRouter 45 | } 46 | } 47 | } 48 | ) 49 | 50 | await flushPromises() 51 | 52 | await wrapper.find('#username').setValue('username') 53 | await wrapper.find('#password').setValue('password') 54 | await wrapper.find('#btn_login').trigger('click') 55 | 56 | await flushPromises() 57 | 58 | expect(showError).toHaveBeenCalledTimes(0) 59 | expect(username).toBe('username') 60 | expect(password).toBe('password') 61 | expect(mockRouter.push).toHaveBeenCalledWith('/') 62 | 63 | wrapper.unmount() 64 | }) 65 | -------------------------------------------------------------------------------- /www/tests/unit/Logs.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import axios from 'axios' 3 | import MockAdapter from 'axios-mock-adapter' 4 | import flushPromises from 'flush-promises' 5 | import Logs from '../../src/views/Logs.vue' 6 | 7 | jest.setTimeout(30000) 8 | 9 | test('Logs', async () => { 10 | const showError = jest.fn() 11 | 12 | const mock = new MockAdapter(axios) 13 | mock.onGet('/rest/logs').reply(200, 14 | { 15 | data: [ 16 | 'log 1', 17 | 'log 2' 18 | ], 19 | success: true 20 | } 21 | ) 22 | const wrapper = mount(Logs, 23 | { 24 | attachTo: document.body, 25 | global: { 26 | stubs: { 27 | Error: { 28 | template: '', 29 | methods: { 30 | showAxios: showError 31 | } 32 | }, 33 | Dialog: true 34 | } 35 | } 36 | } 37 | ) 38 | 39 | await flushPromises() 40 | 41 | await expect(wrapper.find('#logs').text()).toBe('log 1log 2') 42 | 43 | expect(showError).toHaveBeenCalledTimes(0) 44 | wrapper.unmount() 45 | }) 46 | -------------------------------------------------------------------------------- /www/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["vite/client", "@types/jest"], 4 | }, 5 | } -------------------------------------------------------------------------------- /www/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import AutoImport from 'unplugin-auto-import/vite' 4 | import Components from 'unplugin-vue-components/vite' 5 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | AutoImport({ 10 | resolvers: [ElementPlusResolver()] 11 | }), 12 | Components({ 13 | resolvers: [ElementPlusResolver()] 14 | }), 15 | vue() 16 | ] 17 | }) 18 | -------------------------------------------------------------------------------- /www/vue.config.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syncloud/platform/6334691a9f66fe3fa92208fd9109bfe84ff8101b/www/vue.config.js --------------------------------------------------------------------------------