Skip to content

Commit

Permalink
Merge pull request #9 from m1k1o/persistent-data
Browse files Browse the repository at this point in the history
Allow persistent data for container
  • Loading branch information
m1k1o authored May 22, 2021
2 parents 98d56f4 + dcdd2ab commit a024f8f
Show file tree
Hide file tree
Showing 13 changed files with 363 additions and 11 deletions.
18 changes: 18 additions & 0 deletions OpenApi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,20 @@ components:
format: datetime
example: "2021-03-07T21:56:34Z"

RoomMount:
type: object
properties:
type:
type: string
enum: [ private, template, public ]
example: private
host_path:
type: string
example: /profile
container_path:
type: string
example: /home/neko/.config/chromium

RoomSettings:
type: object
properties:
Expand Down Expand Up @@ -291,6 +305,10 @@ components:
type: string
example:
CUSTOM_ENV: custom value
mounts:
type: array
items:
$ref: '#/components/schemas/RoomMount'

RoomStats:
type: object
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ docker-compose up -d
- [x] add GUI
- [x] add HTTPS support
- [x] add authentication provider for traefik
- [x] allow specifying custom ENV variables (TODO: FE.)
- [ ] allow mounting direcotries for persistent data
- [x] allow specifying custom ENV variables
- [x] allow mounting direcotries for persistent data
- [ ] add upgrade button
- [ ] auto pull images, that do not exist
- [ ] add bearer token to for API
Expand Down
2 changes: 1 addition & 1 deletion client/src/api/.openapi-generator/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
5.1.1-SNAPSHOT
5.2.0-SNAPSHOT
42 changes: 42 additions & 0 deletions client/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,42 @@ export interface RoomMember {
*/
muted?: boolean;
}
/**
*
* @export
* @interface RoomMount
*/
export interface RoomMount {
/**
*
* @type {string}
* @memberof RoomMount
*/
type?: RoomMountTypeEnum;
/**
*
* @type {string}
* @memberof RoomMount
*/
host_path?: string;
/**
*
* @type {string}
* @memberof RoomMount
*/
container_path?: string;
}

/**
* @export
* @enum {string}
*/
export enum RoomMountTypeEnum {
private = 'private',
template = 'template',
public = 'public'
}

/**
*
* @export
Expand Down Expand Up @@ -203,6 +239,12 @@ export interface RoomSettings {
* @memberof RoomSettings
*/
envs?: { [key: string]: string; };
/**
*
* @type {Array<RoomMount>}
* @memberof RoomSettings
*/
mounts?: Array<RoomMount>;
}
/**
*
Expand Down
9 changes: 9 additions & 0 deletions client/src/components/RoomInfo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,15 @@
</tbody>
</template>
</v-simple-table>

<div class="my-3 headline">Mounts</div>
<v-simple-table>
<template v-slot:default>
<tbody>
<tr v-for="({ host_path, container_path }, index) in settings.mounts" :key="index"><td style="width:50%;">{{ host_path }}</td><td>{{ container_path }}</td></tr>
</tbody>
</template>
</v-simple-table>
</template>
</div>
</template>
Expand Down
80 changes: 75 additions & 5 deletions client/src/components/RoomsCreate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<v-text-field
label="Name"
v-model="data.name"
:rules="[ rules.slug ]"
:rules="[ rules.minLen(2), rules.containerNameStart, rules.containerName ]"
autocomplete="off"
></v-text-field>
</v-col>
Expand Down Expand Up @@ -187,15 +187,16 @@
<v-btn @click="addEnv" icon color="green"><v-icon>mdi-plus</v-icon></v-btn>
</v-row>
<v-row align="center" v-for="({ key, val }, index) in envList" :key="index">
<v-col class="pb-0">
<v-col class="py-0">
<v-text-field
label="Key"
:value="key"
@input="setEnv(index, { key: $event, val })"
autocomplete="off"
></v-text-field>
</v-col>
<v-col class="pb-0">
<div> : </div>
<v-col class="py-0">
<v-text-field
label="Value"
:value="val"
Expand All @@ -207,6 +208,49 @@
<v-btn @click="delEnv(index)" icon color="red"><v-icon>mdi-close</v-icon></v-btn>
</div>
</v-row>
<v-row align="center" no-gutters class="mt-3">
<h2> Mounts </h2>
<v-btn @click="data.mounts = [ ...data.mounts, { type: 'private', host_path: '', container_path: '' }]" icon color="green"><v-icon>mdi-plus</v-icon></v-btn>
</v-row>
<v-row align="center" class="mb-2" v-for="({ type, host_path, container_path }, index) in data.mounts" :key="index">
<v-col class="py-0" cols="2">
<v-select
label="Type"
:items="mountTypes"
:value="type"
@input="$set(data.mounts, index, { type: $event, host_path, container_path })"
></v-select>
</v-col>
<v-col class="py-0 pl-0">
<v-text-field
label="Host path"
:value="host_path"
@input="$set(data.mounts, index, { type, host_path: $event, container_path })"
:rules="[ rules.absolutePath ]"
autocomplete="off"
></v-text-field>
</v-col>
<div> : </div>
<v-col class="py-0">
<v-text-field
label="Container path"
:value="container_path"
@input="$set(data.mounts, index, { type, host_path, container_path: $event})"
:rules="[ rules.absolutePath ]"
autocomplete="off"
></v-text-field>
</v-col>
<div>
<v-btn @click="$delete(data.mounts, index)" icon color="red"><v-icon>mdi-close</v-icon></v-btn>
</div>
</v-row>
<v-row align="center" no-gutters v-if="data.mounts.length > 0">
<p>
<strong>Private</strong>: Host path is relative to <code class="mx-1">&lt;storage path&gt;/rooms/&lt;room name&gt;/</code>. <br />
<strong>Template</strong>: Host path is relative to <code class="mx-1">&lt;storage path&gt;/templates/</code> and is readonly. <br />
<strong>Public</strong>: Host path must be whitelisted in config and exists on the host.
</p>
</v-row>
</template>
</v-form>
</v-card-text>
Expand Down Expand Up @@ -272,15 +316,24 @@ export default class RoomsCreate extends Vue {
required(val: any) {
return val === null || typeof val === 'undefined' || val === "" ? 'This filed is mandatory.' : true
},
minLen: (min: number) =>
(val: string) =>
val ? (val.length >= min || 'This field must have atleast ' + min + ' characters') : true,
onlyPositive(val: number) {
return val < 0 ? 'Value cannot be negative.' : true
},
nonZero(val: string) {
return val === "0" ? 'Value cannot be zero.' : true
},
slug(val: string) {
return val && !/^[A-Za-z0-9-_.]+$/.test(val) ? 'Should only contain A-Z a-z 0-9 - _ .' : true
containerName(val: string) {
return val && !/^[a-zA-Z0-9_.-]+$/.test(val) ? 'Must only contain a-z A-Z 0-9 _ . -' : true
},
containerNameStart(val: string) {
return val && /^[_.-]/.test(val) ? 'Cannot start with _ . -' : true
},
absolutePath(val: string) {
return val[0] !== "/" ? 'Must be absolute path, starting with /.' : true
}
}
get nekoImages() {
Expand All @@ -299,6 +352,23 @@ export default class RoomsCreate extends Vue {
return this.$store.state.availableScreens
}
get mountTypes() {
return [
{
text: 'Private',
value: 'private',
},
{
text: 'Template',
value: 'template',
},
{
text: 'Public',
value: 'public',
},
]
}
addEnv() {
Vue.set(this, 'envList', [ ...this.envList, { key: '', val: '' } ])
}
Expand Down
5 changes: 5 additions & 0 deletions client/src/store/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ export const state = {

// eslint-disable-next-line
broadcast_pipeline: '',

// eslint-disable-next-line
envs: {},
// eslint-disable-next-line
mounts[],
} as RoomSettings,
videoCodecs: [
"VP8",
Expand Down
2 changes: 2 additions & 0 deletions dev/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
data
ext
12 changes: 11 additions & 1 deletion dev/start
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,22 @@ fi

export $(cat ../.env | sed 's/#.*//g' | xargs)

DATA_PATH="./data"
mkdir -p "${DATA_PATH}"

EXTERNAL_PATH="./ext"
mkdir -p "${EXTERNAL_PATH}"

docker run --rm -it \
--name="neko_rooms_server" \
-v "${PWD}/../:/app" \
-v "`realpath ..`:/app" \
-v "`realpath ${DATA_PATH}`:/data" \
-e "TZ=${TZ}" \
-e "NEKO_ROOMS_EPR=${NEKO_ROOMS_EPR}" \
-e "NEKO_ROOMS_NAT1TO1=${NEKO_ROOMS_NAT1TO1}" \
-e "NEKO_ROOMS_STORAGE_INTERNAL=/data" \
-e "NEKO_ROOMS_STORAGE_EXTERNAL=`realpath ${DATA_PATH}`" \
-e "NEKO_ROOMS_MOUNTS_WHITELIST=`realpath ${EXTERNAL_PATH}`" \
-e "NEKO_ROOMS_TRAEFIK_DOMAIN=${NEKO_ROOMS_TRAEFIK_DOMAIN}" \
-e "NEKO_ROOMS_TRAEFIK_ENTRYPOINT=${NEKO_ROOMS_TRAEFIK_ENTRYPOINT}" \
-e "NEKO_ROOMS_TRAEFIK_NETWORK=${NEKO_ROOMS_TRAEFIK_NETWORK}" \
Expand Down
59 changes: 59 additions & 0 deletions internal/config/room.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package config

import (
"path/filepath"
"strconv"
"strings"

dockerNames "github.com/docker/docker/daemon/names"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
Expand All @@ -16,6 +18,12 @@ type Room struct {
NAT1To1IPs []string
NekoImages []string

StorageEnabled bool
StorageInternal string
StorageExternal string

MountsWhitelist []string

InstanceName string
InstanceUrl string

Expand Down Expand Up @@ -50,6 +58,28 @@ func (Room) Init(cmd *cobra.Command) error {
return err
}

// Data

cmd.PersistentFlags().Bool("storage.enabled", true, "whether storage is enabled, where peristent containers data will be stored")
if err := viper.BindPFlag("storage.enabled", cmd.PersistentFlags().Lookup("storage.enabled")); err != nil {
return err
}

cmd.PersistentFlags().String("storage.external", "", "external absolute path (on the host) to storage folder")
if err := viper.BindPFlag("storage.external", cmd.PersistentFlags().Lookup("storage.external")); err != nil {
return err
}

cmd.PersistentFlags().String("storage.internal", "/data", "internal absolute path (inside container) to storage folder")
if err := viper.BindPFlag("storage.internal", cmd.PersistentFlags().Lookup("storage.internal")); err != nil {
return err
}

cmd.PersistentFlags().StringSlice("mounts.whitelist", []string{}, "whitelisted public mounts for containers")
if err := viper.BindPFlag("mounts.whitelist", cmd.PersistentFlags().Lookup("mounts.whitelist")); err != nil {
return err
}

// Instance

cmd.PersistentFlags().String("instance.name", "neko-rooms", "unique instance name (if running muliple on the same host)")
Expand Down Expand Up @@ -120,7 +150,36 @@ func (s *Room) Set() {
s.NAT1To1IPs = viper.GetStringSlice("nat1to1")
s.NekoImages = viper.GetStringSlice("neko_images")

s.StorageEnabled = viper.GetBool("storage.enabled")
s.StorageInternal = viper.GetString("storage.internal")
s.StorageExternal = viper.GetString("storage.external")

if s.StorageInternal != "" && s.StorageExternal != "" {
s.StorageInternal = filepath.Clean(s.StorageInternal)
s.StorageExternal = filepath.Clean(s.StorageExternal)

if !filepath.IsAbs(s.StorageInternal) || !filepath.IsAbs(s.StorageExternal) {
log.Panic().Msg("invalid `storage.internal` or `storage.external`, must be an absolute path")
}
} else {
log.Warn().Msg("missing `storage.internal` or `storage.external`, storage is unavailable")
s.StorageEnabled = false
}

s.MountsWhitelist = viper.GetStringSlice("mounts.whitelist")
for _, path := range s.MountsWhitelist {
path = filepath.Clean(path)

if !filepath.IsAbs(path) {
log.Panic().Msg("invalid `mounts.whitelist`, must be an absolute path")
}
}

s.InstanceName = viper.GetString("instance.name")
if !dockerNames.RestrictedNamePattern.MatchString(s.InstanceName) {
log.Panic().Msg("invalid `instance.name`, must match " + dockerNames.RestrictedNameChars)
}

s.InstanceUrl = viper.GetString("instance.url")

s.TraefikDomain = viper.GetString("traefik.domain")
Expand Down
Loading

0 comments on commit a024f8f

Please sign in to comment.