web ui: allow to create folders from a template

This commit is contained in:
Nicola Murino 2021-02-04 19:09:43 +01:00
parent 17a42a0c11
commit 267d9f1831
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
13 changed files with 268 additions and 51 deletions

View file

@ -677,7 +677,7 @@ func (p *BoltProvider) getFolderByName(name string) (vfs.BaseVirtualFolder, erro
}
func (p *BoltProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
err := validateFolder(folder)
err := ValidateFolder(folder)
if err != nil {
return err
}
@ -696,7 +696,7 @@ func (p *BoltProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
}
func (p *BoltProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {
err := validateFolder(folder)
err := ValidateFolder(folder)
if err != nil {
return err
}

View file

@ -997,7 +997,7 @@ func validateFolderQuotaLimits(folder vfs.VirtualFolder) error {
}
func getVirtualFolderIfInvalid(folder *vfs.BaseVirtualFolder) *vfs.BaseVirtualFolder {
if err := validateFolder(folder); err == nil {
if err := ValidateFolder(folder); err == nil {
return folder
}
// we try to get the folder from the data provider if only the Name is populated
@ -1029,7 +1029,7 @@ func validateUserVirtualFolders(user *User) error {
return err
}
folder := getVirtualFolderIfInvalid(&v.BaseVirtualFolder)
if err := validateFolder(folder); err != nil {
if err := ValidateFolder(folder); err != nil {
return err
}
cleanedMPath := folder.MappedPath
@ -1388,7 +1388,9 @@ func createUserPasswordHash(user *User) error {
return nil
}
func validateFolder(folder *vfs.BaseVirtualFolder) error {
// ValidateFolder returns an error if the folder is not valid
// FIXME: this should be defined as Folder struct method
func ValidateFolder(folder *vfs.BaseVirtualFolder) error {
if folder.Name == "" {
return &ValidationError{err: "folder name is mandatory"}
}

View file

@ -651,7 +651,7 @@ func (p *MemoryProvider) getFolderByName(name string) (vfs.BaseVirtualFolder, er
}
func (p *MemoryProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
err := validateFolder(folder)
err := ValidateFolder(folder)
if err != nil {
return err
}
@ -675,7 +675,7 @@ func (p *MemoryProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
}
func (p *MemoryProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {
err := validateFolder(folder)
err := ValidateFolder(folder)
if err != nil {
return err
}

View file

@ -669,7 +669,7 @@ func sqlCommonAddOrGetFolder(ctx context.Context, baseFolder vfs.BaseVirtualFold
}
func sqlCommonAddFolder(folder *vfs.BaseVirtualFolder, dbHandle sqlQuerier) error {
err := validateFolder(folder)
err := ValidateFolder(folder)
if err != nil {
return err
}
@ -688,7 +688,7 @@ func sqlCommonAddFolder(folder *vfs.BaseVirtualFolder, dbHandle sqlQuerier) erro
}
func sqlCommonUpdateFolder(folder *vfs.BaseVirtualFolder, dbHandle *sql.DB) error {
err := validateFolder(folder)
err := ValidateFolder(folder)
if err != nil {
return err
}

View file

@ -4,7 +4,6 @@ import (
"context"
"errors"
"net/http"
"strconv"
"github.com/go-chi/jwtauth"
"github.com/go-chi/render"
@ -18,36 +17,9 @@ type adminPwd struct {
}
func getAdmins(w http.ResponseWriter, r *http.Request) {
limit := 100
offset := 0
order := dataprovider.OrderASC
var err error
if _, ok := r.URL.Query()["limit"]; ok {
limit, err = strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
err = errors.New("Invalid limit")
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
if limit > 500 {
limit = 500
}
}
if _, ok := r.URL.Query()["offset"]; ok {
offset, err = strconv.Atoi(r.URL.Query().Get("offset"))
if err != nil {
err = errors.New("Invalid offset")
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
}
if _, ok := r.URL.Query()["order"]; ok {
order = r.URL.Query().Get("order")
if order != dataprovider.OrderASC && order != dataprovider.OrderDESC {
err = errors.New("Invalid order")
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
limit, offset, order, err := getSearchFilters(w, r)
if err != nil {
return
}
admins, err := dataprovider.GetAdmins(limit, offset, order)

View file

@ -65,6 +65,7 @@ const (
webQuotaScanPath = "/web/quota-scans"
webChangeAdminPwdPath = "/web/changepwd/admin"
webTemplateUser = "/web/template/user"
webTemplateFolder = "/web/template/folder"
webStaticFilesPath = "/static"
// MaxRestoreSize defines the max size for the loaddata input file
MaxRestoreSize = 10485760 // 10 MB

View file

@ -81,6 +81,7 @@ const (
webRestorePath = "/web/restore"
webChangeAdminPwdPath = "/web/changepwd/admin"
webTemplateUser = "/web/template/user"
webTemplateFolder = "/web/template/folder"
httpBaseURL = "http://127.0.0.1:8081"
configDir = ".."
httpsCert = `-----BEGIN CERTIFICATE-----
@ -2325,6 +2326,11 @@ func TestProviderErrors(t *testing.T) {
setJWTCookieForReq(req, testServerToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusInternalServerError, rr)
req, err = http.NewRequest(http.MethodGet, webTemplateFolder+"?from=afolder", nil)
assert.NoError(t, err)
setJWTCookieForReq(req, testServerToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusInternalServerError, rr)
err = config.LoadConfig(configDir, "")
assert.NoError(t, err)
providerConf := config.GetProviderConf()
@ -4803,6 +4809,38 @@ func TestWebUserUpdateMock(t *testing.T) {
checkResponseCode(t, http.StatusOK, rr)
}
func TestRenderFolderTemplateMock(t *testing.T) {
token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, webTemplateFolder, nil)
assert.NoError(t, err)
setJWTCookieForReq(req, token)
rr := executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
folder := vfs.BaseVirtualFolder{
Name: "templatefolder",
MappedPath: filepath.Join(os.TempDir(), "mapped"),
}
folder, _, err = httpdtest.AddFolder(folder, http.StatusCreated)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodGet, webTemplateFolder+fmt.Sprintf("?from=%v", folder.Name), nil)
assert.NoError(t, err)
setJWTCookieForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
req, err = http.NewRequest(http.MethodGet, webTemplateFolder+"?from=unknown-folder", nil)
assert.NoError(t, err)
setJWTCookieForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusNotFound, rr)
_, err = httpdtest.RemoveFolder(folder, http.StatusOK)
assert.NoError(t, err)
}
func TestRenderUserTemplateMock(t *testing.T) {
token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err)
@ -4976,7 +5014,7 @@ func TestUserTemplateMock(t *testing.T) {
// test invalid s3_upload_part_size
form.Set("s3_upload_part_size", "a")
b, contentType, _ := getMultipartFormData(form, "", "")
req, _ := http.NewRequest(http.MethodPost, path.Join(webTemplateUser), &b)
req, _ := http.NewRequest(http.MethodPost, webTemplateUser, &b)
setJWTCookieForReq(req, token)
req.Header.Set("Content-Type", contentType)
rr := executeRequest(req)
@ -4985,7 +5023,7 @@ func TestUserTemplateMock(t *testing.T) {
form.Set("s3_upload_concurrency", strconv.Itoa(user.FsConfig.S3Config.UploadConcurrency))
b, contentType, _ = getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, path.Join(webTemplateUser), &b)
req, _ = http.NewRequest(http.MethodPost, webTemplateUser, &b)
setJWTCookieForReq(req, token)
req.Header.Set("Content-Type", contentType)
rr = executeRequest(req)
@ -4993,7 +5031,7 @@ func TestUserTemplateMock(t *testing.T) {
form.Set("users", "user1::password1::invalid-pkey")
b, contentType, _ = getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, path.Join(webTemplateUser), &b)
req, _ = http.NewRequest(http.MethodPost, webTemplateUser, &b)
setJWTCookieForReq(req, token)
req.Header.Set("Content-Type", contentType)
rr = executeRequest(req)
@ -5002,7 +5040,7 @@ func TestUserTemplateMock(t *testing.T) {
form.Set("users", "user1:password1")
b, contentType, _ = getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, path.Join(webTemplateUser), &b)
req, _ = http.NewRequest(http.MethodPost, webTemplateUser, &b)
setJWTCookieForReq(req, token)
req.Header.Set("Content-Type", contentType)
rr = executeRequest(req)
@ -5011,7 +5049,7 @@ func TestUserTemplateMock(t *testing.T) {
form.Set("users", "user1::password1\nuser2::password2::"+testPubKey+"\nuser3::::")
b, contentType, _ = getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, path.Join(webTemplateUser), &b)
req, _ = http.NewRequest(http.MethodPost, webTemplateUser, &b)
setJWTCookieForReq(req, token)
req.Header.Set("Content-Type", contentType)
rr = executeRequest(req)
@ -5021,6 +5059,8 @@ func TestUserTemplateMock(t *testing.T) {
err = json.Unmarshal(rr.Body.Bytes(), &dump)
require.NoError(t, err)
require.Len(t, dump.Users, 2)
require.Len(t, dump.Admins, 0)
require.Len(t, dump.Folders, 0)
user1 := dump.Users[0]
user2 := dump.Users[1]
require.Equal(t, "user1", user1.Username)
@ -5044,6 +5084,71 @@ func TestUserTemplateMock(t *testing.T) {
require.Equal(t, "password2", user2.FsConfig.S3Config.AccessSecret.GetPayload())
}
func TestFolderTemplateMock(t *testing.T) {
folderName := "vfolder-template"
mappedPath := filepath.Join(os.TempDir(), "%name%mapped%name%path")
token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err)
csrfToken, err := getCSRFToken()
assert.NoError(t, err)
form := make(url.Values)
form.Set("name", folderName)
form.Set("mapped_path", mappedPath)
form.Set("folders", "folder1\nfolder2\nfolder3\nfolder1\n\n\n")
contentType := "application/x-www-form-urlencoded"
req, _ := http.NewRequest(http.MethodPost, webTemplateFolder, bytes.NewBuffer([]byte(form.Encode())))
setJWTCookieForReq(req, token)
req.Header.Set("Content-Type", contentType)
rr := executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "Unable to verify form token")
form.Set(csrfFormToken, csrfToken)
req, _ = http.NewRequest(http.MethodPost, webTemplateFolder+"?param=p%C3%AO%GG", bytes.NewBuffer([]byte(form.Encode())))
setJWTCookieForReq(req, token)
req.Header.Set("Content-Type", contentType)
rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "Error parsing folders fields")
req, _ = http.NewRequest(http.MethodPost, webTemplateFolder, bytes.NewBuffer([]byte(form.Encode())))
setJWTCookieForReq(req, token)
req.Header.Set("Content-Type", contentType)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
var dump dataprovider.BackupData
err = json.Unmarshal(rr.Body.Bytes(), &dump)
require.NoError(t, err)
require.Len(t, dump.Users, 0)
require.Len(t, dump.Admins, 0)
require.Len(t, dump.Folders, 3)
require.Equal(t, "folder1", dump.Folders[0].Name)
require.True(t, strings.HasSuffix(dump.Folders[0].MappedPath, "folder1mappedfolder1path"))
require.Equal(t, "folder2", dump.Folders[1].Name)
require.True(t, strings.HasSuffix(dump.Folders[1].MappedPath, "folder2mappedfolder2path"))
require.Equal(t, "folder3", dump.Folders[2].Name)
require.True(t, strings.HasSuffix(dump.Folders[2].MappedPath, "folder3mappedfolder3path"))
form.Set("folders", "\n\n\n")
req, _ = http.NewRequest(http.MethodPost, webTemplateFolder, bytes.NewBuffer([]byte(form.Encode())))
setJWTCookieForReq(req, token)
req.Header.Set("Content-Type", contentType)
rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "No folders to export")
form.Set("folders", "name")
form.Set("mapped_path", "relative-path")
req, _ = http.NewRequest(http.MethodPost, webTemplateFolder, bytes.NewBuffer([]byte(form.Encode())))
setJWTCookieForReq(req, token)
req.Header.Set("Content-Type", contentType)
rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "Error validating folder")
}
func TestWebUserS3Mock(t *testing.T) {
webToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err)

View file

@ -392,6 +392,9 @@ func (s *httpdServer) initializeRouter() {
router.With(checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie).
Get(webTemplateUser, handleWebTemplateUserGet)
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Post(webTemplateUser, handleWebTemplateUserPost)
router.With(checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie).
Get(webTemplateFolder, handleWebTemplateFolderGet)
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Post(webTemplateFolder, handleWebTemplateFolderPost)
})
router.Group(func(router chi.Router) {

View file

@ -36,6 +36,7 @@ type folderPageMode int
const (
folderPageModeAdd folderPageMode = iota + 1
folderPageModeUpdate
folderPageModeTemplate
)
const (
@ -88,6 +89,7 @@ type basePage struct {
ConnectionsURL string
FoldersURL string
FolderURL string
FolderTemplateURL string
LogoutURL string
ChangeAdminPwdURL string
FolderQuotaScanURL string
@ -277,6 +279,7 @@ func getBasePageData(title, currentURL string, r *http.Request) basePage {
AdminURL: webAdminPath,
FoldersURL: webFoldersPath,
FolderURL: webFolderPath,
FolderTemplateURL: webTemplateFolder,
LogoutURL: webLogoutPath,
ChangeAdminPwdURL: webChangeAdminPwdPath,
QuotaScanURL: webQuotaScanPath,
@ -409,6 +412,9 @@ func renderFolderPage(w http.ResponseWriter, r *http.Request, folder vfs.BaseVir
case folderPageModeUpdate:
title = "Update folder"
currentURL = fmt.Sprintf("%v/%v", webFolderPath, url.PathEscape(folder.Name))
case folderPageModeTemplate:
title = "Folder template"
currentURL = webTemplateFolder
}
data := folderPage{
basePage: getBasePageData(title, currentURL, r),
@ -419,6 +425,20 @@ func renderFolderPage(w http.ResponseWriter, r *http.Request, folder vfs.BaseVir
renderTemplate(w, templateFolder, data)
}
func getFoldersForTemplate(r *http.Request) []string {
var res []string
formValue := r.Form.Get("folders")
folders := make(map[string]bool)
for _, name := range getSliceFromDelimitedValues(formValue, "\n") {
if _, ok := folders[name]; ok {
continue
}
folders[name] = true
res = append(res, name)
}
return res
}
func getUsersForTemplate(r *http.Request) []userTemplateFields {
var res []userTemplateFields
formValue := r.Form.Get("users")
@ -789,6 +809,16 @@ func replacePlaceholders(field string, replacements map[string]string) string {
return field
}
func getFolderFromTemplate(folder vfs.BaseVirtualFolder, name string) vfs.BaseVirtualFolder {
folder.Name = name
replacements := make(map[string]string)
replacements["%name%"] = folder.Name
folder.MappedPath = replacePlaceholders(folder.MappedPath, replacements)
return folder
}
func getCryptFsFromTemplate(fsConfig vfs.CryptFsConfig, replacements map[string]string) vfs.CryptFsConfig {
if fsConfig.Passphrase != nil {
if fsConfig.Passphrase.IsPlain() {
@ -1185,6 +1215,61 @@ func handleGetWebUsers(w http.ResponseWriter, r *http.Request) {
renderTemplate(w, templateUsers, data)
}
func handleWebTemplateFolderGet(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("from") != "" {
name := r.URL.Query().Get("from")
folder, err := dataprovider.GetFolderByName(name)
if err == nil {
renderFolderPage(w, r, folder, folderPageModeTemplate, "")
} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
renderNotFoundPage(w, r, err)
} else {
renderInternalServerErrorPage(w, r, err)
}
} else {
folder := vfs.BaseVirtualFolder{}
renderFolderPage(w, r, folder, folderPageModeTemplate, "")
}
}
func handleWebTemplateFolderPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
templateFolder := vfs.BaseVirtualFolder{}
err := r.ParseForm()
if err != nil {
renderMessagePage(w, r, "Error parsing folders fields", "", http.StatusBadRequest, err, "")
return
}
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
renderForbiddenPage(w, r, err.Error())
return
}
templateFolder.MappedPath = r.Form.Get("mapped_path")
var dump dataprovider.BackupData
dump.Version = dataprovider.DumpVersion
foldersFields := getFoldersForTemplate(r)
for _, tmpl := range foldersFields {
f := getFolderFromTemplate(templateFolder, tmpl)
if err := dataprovider.ValidateFolder(&f); err != nil {
renderMessagePage(w, r, fmt.Sprintf("Error validating folder %#v", f.Name), "", http.StatusBadRequest, err, "")
return
}
dump.Folders = append(dump.Folders, f)
}
if len(dump.Folders) == 0 {
renderMessagePage(w, r, "No folders to export", "No valid folders found, export is not possible", http.StatusBadRequest, nil, "")
return
}
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"sftpgo-%v-folders-from-template.json\"", len(dump.Folders)))
render.JSON(w, r, dump)
}
func handleWebTemplateUserGet(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("from") != "" {
username := r.URL.Query().Get("from")

View file

@ -5,7 +5,7 @@
{{define "page_body"}}
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Add a new folder</h6>
<h6 class="m-0 font-weight-bold text-primary">{{.Title}}</h6>
</div>
<div class="card-body">
{{if .Error}}
@ -13,7 +13,34 @@
<div class="card-body text-form-error">{{.Error}}</div>
</div>
{{end}}
<form id="folder_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
{{if eq .Mode 3}}
<div class="card mb-4 border-left-info">
<div class="card-body">
Generate a data provider independent JSON file to create new folders or update existing ones.
<br>
The following placeholder is supported:
<br><br>
<ul>
<li><span class="text-success">%name%</span> will be replaced with the specified folder name</li>
</ul>
The generated folders file can be imported from the "Maintenance" section.
</div>
</div>
{{end}}
<form id="folder_form" action="{{.CurrentURL}}" method="POST" autocomplete="off" {{if eq .Mode 3}}target="_blank"{{end}}>
{{if eq .Mode 3}}
<div class="form-group row">
<label for="idFolders" class="col-sm-2 col-form-label">Folders</label>
<div class="col-sm-10">
<textarea class="form-control" id="idFolders" name="folders" rows="5" required
aria-describedby="foldersHelpBlock"></textarea>
<small id="foldersHelpBlock" class="form-text text-muted">
Specify the folder names, one for line.
</small>
</div>
</div>
<input type="hidden" name="name" id="idFolderName" value="{{.Folder.Name}}">
{{else}}
<div class="form-group row">
<label for="idFolderName" class="col-sm-2 col-form-label">Name</label>
<div class="col-sm-10">
@ -21,6 +48,7 @@
value="{{.Folder.Name}}" maxlength="255" autocomplete="nope" required {{if ge .Mode 2}}readonly{{end}}>
</div>
</div>
{{end}}
<div class="form-group row">
<label for="idMappedPath" class="col-sm-2 col-form-label">Absolute Path</label>
<div class="col-sm-10">
@ -30,7 +58,7 @@
</div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Submit</button>
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">{{if eq .Mode 3}}Generate and export folders{{else}}Submit{{end}}</button>
</form>
</div>
</div>

View file

@ -141,6 +141,21 @@ function deleteAction() {
enabled: false
};
$.fn.dataTable.ext.buttons.template = {
text: 'Template',
name: 'template',
action: function (e, dt, node, config) {
var selectedRows = table.rows({ selected: true }).count();
if (selectedRows == 1){
var folderName = table.row({ selected: true }).data()[0];
var path = '{{.FolderTemplateURL}}' + "?from=" + encodeURIComponent(folderName);
window.location.href = path;
} else {
window.location.href = '{{.FolderTemplateURL}}';
}
}
};
$.fn.dataTable.ext.buttons.delete = {
text: 'Delete',
name: 'delete',
@ -213,6 +228,10 @@ function deleteAction() {
table.button().add(0,'quota_scan');
{{end}}
{{if .LoggedAdmin.HasPermission "manage_system"}}
table.button().add(0,'template');
{{end}}
{{if .LoggedAdmin.HasPermission "del_users"}}
table.button().add(0,'delete');
{{end}}

View file

@ -37,8 +37,7 @@ type webDavServer struct {
binding Binding
}
func (s *webDavServer) listenAndServe() error {
compressor := middleware.NewCompressor(5, "text/*")
func (s *webDavServer) listenAndServe(compressor *middleware.Compressor) error {
handler := compressor.Handler(s)
httpServer := &http.Server{
Addr: s.binding.GetAddress(),

View file

@ -5,6 +5,8 @@ import (
"fmt"
"path/filepath"
"github.com/go-chi/chi/middleware"
"github.com/drakkan/sftpgo/common"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/utils"
@ -157,6 +159,7 @@ func (c *Configuration) Initialize(configDir string) error {
}
certMgr = mgr
}
compressor := middleware.NewCompressor(5, "text/*")
serviceStatus = ServiceStatus{
Bindings: nil,
@ -174,7 +177,7 @@ func (c *Configuration) Initialize(configDir string) error {
config: c,
binding: binding,
}
exitChannel <- server.listenAndServe()
exitChannel <- server.listenAndServe(compressor)
}(binding)
}