Browse Source

Files manager popup, js, css, actions, migration

Vova Tkach 5 years ago
parent
commit
4420951e07

+ 1 - 0
Makefile

@@ -28,6 +28,7 @@ build: clean version template dockerfile
 	@cp -R ./hosts/localhost ./bin/localhost
 	@-find ./bin/localhost -type f -name '.*' -exec rm -f {} \;
 	@-rm -R ./bin/localhost/htdocs/products
+	@-rm -R ./bin/localhost/htdocs/public
 	@-rm ./bin/localhost/config/*
 	@-rm ./bin/localhost/htdocs/*
 	@-rm ./bin/localhost/logs/*

+ 193 - 5
engine/assets/cp.scripts.js

@@ -6992,7 +6992,6 @@
 			var html = '<div class="modal fade" id="sys-modal-system-message" tabindex="-1" role="dialog" aria-labelledby="sysModalSystemMessageLabel" aria-hidden="true"> \
 				<div class="modal-dialog modal-dialog-centered" role="document"> \
 					<div class="modal-content"> \
-							<input type="hidden" name="action" value="index-user-update-profile"> \
 							<div class="modal-header"> \
 								<h5 class="modal-title" id="sysModalSystemMessageLabel">' + title + '</h5> \
 								<button type="button" class="close" data-dismiss="modal" aria-label="Close"> \
@@ -7504,9 +7503,6 @@
 
 			ShopProductsDeleteImage: function(button, product_id, filename) {
 				if($(button).hasClass('in-progress')) return;
-				// if(!confirm('Are you sure want to delete image?')) {
-				// 	return;
-				// }
 				$(button).addClass('in-progress');
 				$.ajax({
 					type: "POST",
@@ -7658,7 +7654,6 @@
 				var html = '<div class="modal fade" id="sys-modal-shop-product-attach" tabindex="-1" role="dialog" aria-labelledby="sysModalShopProductLabel" aria-hidden="true"> \
 					<div class="modal-dialog modal-dialog-centered" role="document"> \
 						<div class="modal-content"> \
-							<input type="hidden" name="action" value="index-user-update-profile"> \
 							<div class="modal-header"> \
 								<h5 class="modal-title" id="sysModalShopProductLabel">Attach product</h5> \
 								<button type="button" class="close" data-dismiss="modal" aria-label="Close"> \
@@ -7759,6 +7754,199 @@
 					});
 				}
 			},
+
+			FilesManagerDialog: function() {
+				var html = '<div class="modal fade" id="sys-modal-files-manager" tabindex="-1" role="dialog" aria-labelledby="sysModalFilesManagerLabel" aria-hidden="true"> \
+					<div class="modal-dialog modal-dialog-centered" role="document"> \
+						<div class="modal-content"> \
+							<input type="hidden" name="path" value="/"> \
+							<div class="modal-header"> \
+								<h5 class="modal-title" id="sysModalFilesManagerLabel">Files manager</h5> \
+								<button type="button" class="close" data-dismiss="modal" aria-label="Close"> \
+									<span aria-hidden="true">&times;</span> \
+								</button> \
+							</div> \
+							<div class="modal-body text-left"> \
+								<div class="dialog-path alert alert-secondary"><span class="text-dotted">/</span></div> \
+								<div class="dialog-data"></div> \
+							</div> \
+							<div class="modal-footer"> \
+								<input class="form-control" type="file" id="fmfiles" name="fmfiles" onchange="fave.FilesManagerUploadFile();" style="font-size:12px;background-color:#28a745;border-color:#28a745;color:#fff;cursor:pointer;" multiple=""> \
+								<button type="button" class="btn btn-primary folder" onclick="fave.FilesManagerNewFolderClick();" disabled>New folder</button> \
+								<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> \
+							</div> \
+						</div> \
+					</div> \
+				</div>';
+				$('#sys-modal-files-manager-placeholder').html(html);
+				$("#sys-modal-files-manager").modal({
+					backdrop: 'static',
+					keyboard: true,
+					show: false,
+				});
+				$('#sys-modal-files-manager').on('hidden.bs.modal', function(e) {
+					$('#sys-modal-files-manager-placeholder').html('');
+				});
+				$("#sys-modal-files-manager").modal('show');
+
+				setTimeout(function() {
+					fave.FilesManagerLoadData('/');
+				}, 500);
+			},
+
+			FilesManagerSetPath: function(path) {
+				$('#sys-modal-files-manager input[name=path]').val(path);
+				$('#sys-modal-files-manager .dialog-path span').html(path);
+			},
+
+			FilesManagerGetPath: function() {
+				return $('#sys-modal-files-manager input[name=path]').val();
+			},
+
+			FilesManagerRemoveFolder: function(filename, msg) {
+				if(!confirm(msg)) {
+					return;
+				}
+				$.ajax({
+					type: "POST",
+					url: '/cp/',
+					data: {
+						action: 'files-remove-folder',
+						file: filename,
+					}
+				}).done(function(data) {
+					if($('#sys-modal-files-manager').length > 0) {
+						if(IsDebugMode()) console.log('done', data);
+						AjaxDone(data);
+					}
+				}).fail(function(xhr, status, error) {
+					if($('#sys-modal-files-manager').length > 0) {
+						if(IsDebugMode()) console.log('fail', xhr, status, error);
+						AjaxFail(xhr.responseText, status, error);
+					}
+				});
+			},
+
+			FilesManagerRemoveFile: function(filename, msg) {
+				if(!confirm(msg)) {
+					return;
+				}
+				$.ajax({
+					type: "POST",
+					url: '/cp/',
+					data: {
+						action: 'files-remove-file',
+						file: filename,
+					}
+				}).done(function(data) {
+					if($('#sys-modal-files-manager').length > 0) {
+						if(IsDebugMode()) console.log('done', data);
+						AjaxDone(data);
+					}
+				}).fail(function(xhr, status, error) {
+					if($('#sys-modal-files-manager').length > 0) {
+						if(IsDebugMode()) console.log('fail', xhr, status, error);
+						AjaxFail(xhr.responseText, status, error);
+					}
+				});
+			},
+
+			FilesManagerLoadData: function(path) {
+				fave.FilesManagerEnableDisableButtons(true);
+				$.ajax({
+					type: "POST",
+					url: '/cp/',
+					data: {
+						action: 'files-list',
+						path: path,
+					}
+				}).done(function(data) {
+					if($('#sys-modal-files-manager').length > 0) {
+						if(IsDebugMode()) console.log('done', data);
+						AjaxDone(data);
+					}
+				}).fail(function(xhr, status, error) {
+					if($('#sys-modal-files-manager').length > 0) {
+						if(IsDebugMode()) console.log('fail', xhr, status, error);
+						AjaxFail(xhr.responseText, status, error);
+					}
+				});
+			},
+
+			FilesManagerLoadDataUp: function(path) {
+				newPath = path.replace(/\/$/i, '');
+				newPath = newPath.replace(/[^\/]+$/i, '');
+				fave.FilesManagerLoadData(newPath);
+			},
+
+			FilesManagerEnableDisableButtons: function(disabled) {
+				$('#sys-modal-files-manager #fmfiles').prop('disabled', disabled);
+				$('#sys-modal-files-manager button.folder').prop('disabled', disabled);
+			},
+
+			FilesManagerUploadFile: function() {
+				var file_el = $('#fmfiles')[0];
+				if(!file_el.files) return;
+				if(file_el.files.length <= 0) return;
+
+				fave.FilesManagerEnableDisableButtons(true);
+
+				var fd = new FormData();
+				fd.append('action', 'files-upload');
+				fd.append('count', file_el.files.length);
+				fd.append('path', fave.FilesManagerGetPath());
+				for(var i = 0; i < file_el.files.length; i++) {
+					fd.append('file_' + i, file_el.files[i]);
+				}
+
+				$.ajax({
+					url: '/cp/',
+					method: 'POST',
+					type: 'POST',
+					data: fd,
+					contentType: false,
+					processData: false
+				}).done(function(data) {
+					if($('#sys-modal-files-manager').length > 0) {
+						if(IsDebugMode()) console.log('done', data);
+						AjaxDone(data);
+					}
+				}).fail(function(xhr, status, error) {
+					if($('#sys-modal-files-manager').length > 0) {
+						if(IsDebugMode()) console.log('fail', xhr, status, error);
+						AjaxFail(xhr.responseText, status, error);
+					}
+				}).always(function() {
+					file_el.value = '';
+					fave.FilesManagerEnableDisableButtons(false);
+				});
+			},
+
+			FilesManagerNewFolderClick: function() {
+				var folderName = prompt('Please enter new folder name', '');
+				if(folderName != null) {
+					path = fave.FilesManagerGetPath();
+					$.ajax({
+						type: "POST",
+						url: '/cp/',
+						data: {
+							action: 'files-mkdir',
+							path: path,
+							name: folderName,
+						}
+					}).done(function(data) {
+						if($('#sys-modal-files-manager').length > 0) {
+							if(IsDebugMode()) console.log('done', data);
+							AjaxDone(data);
+						}
+					}).fail(function(xhr, status, error) {
+						if($('#sys-modal-files-manager').length > 0) {
+							if(IsDebugMode()) console.log('fail', xhr, status, error);
+							AjaxFail(xhr.responseText, status, error);
+						}
+					});
+				}
+			},
 		};
 	}(window, $);
 

File diff suppressed because it is too large
+ 0 - 0
engine/assets/cp.scripts.js.go


+ 28 - 0
engine/assets/cp.styles.css

@@ -1147,6 +1147,24 @@ ul.pagination {
 	display: none;
 }
 
+/* Files manager */
+.data-table.table_fm_files .col_name {
+	max-width: 10rem;
+}
+
+.data-table.table_fm_files .col_type {
+	width: 6rem;
+}
+
+.data-table.table_fm_files .col_action {
+	width: 6rem;
+	text-align: right;
+}
+
+.data-table.table_fm_files .col_name a {
+	display: block;
+}
+
 /* Fix for bootstrap select */
 .dropdown.bootstrap-select {
 	position: relative;
@@ -1253,6 +1271,16 @@ div.wysiwyg.focused {
 	padding-bottom: 0px !important;
 }
 
+/* Dotted text */
+.text-dotted {
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	display: inline-block;
+	max-width: 100%;
+	margin-bottom: -0.5rem;
+}
+
 /* Mobile fixes */
 @media (min-width: 992px) {
 	body.cp.cp-sidebar-right .wrap .sidebar.sidebar-right.d-lg-table-cell {

File diff suppressed because it is too large
+ 0 - 0
engine/assets/cp.styles.css.go


File diff suppressed because it is too large
+ 0 - 0
engine/assets/tmpl.cp.base.go


+ 1 - 1
engine/assets/tmpl.cp.base.html

@@ -63,7 +63,7 @@
 						</div>
 					</li>
 					<li class="nav-item">
-						<a class="nav-link" href="javascript:fave.FilesManager();" role="button" aria-expanded="false">
+						<a class="nav-link" href="javascript:fave.FilesManagerDialog();" role="button" aria-expanded="false">
 							Files
 						</a>
 					</li>

+ 85 - 0
engine/modules/module_files_act_list.go

@@ -0,0 +1,85 @@
+package modules
+
+import (
+	"html"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"golang-fave/engine/assets"
+	"golang-fave/engine/builder"
+	"golang-fave/engine/utils"
+	"golang-fave/engine/wrapper"
+)
+
+func (this *Modules) RegisterAction_FilesList() *Action {
+	return this.newAction(AInfo{
+		Mount:     "files-list",
+		WantAdmin: true,
+	}, func(wrap *wrapper.Wrapper) {
+		pf_path := utils.SafeFilePath(utils.Trim(wrap.R.FormValue("path")))
+
+		// Set path
+		wrap.Write(`fave.FilesManagerSetPath('` + pf_path + `');`)
+
+		// Render table
+		start_dir := strings.Join([]string{wrap.DHtdocs, "public"}, string(os.PathSeparator)) + pf_path + "*"
+
+		str_dirs := ""
+		str_files := ""
+
+		if files, err := filepath.Glob(start_dir); err == nil {
+			for _, file := range files {
+				file_name := file
+				i := strings.LastIndex(file_name, string(os.PathSeparator))
+				if i != -1 {
+					file_name = file_name[i+1:]
+				}
+
+				if utils.IsDir(file) {
+					actions := builder.DataTableAction(&[]builder.DataTableActionRow{
+						{
+							Icon:   assets.SysSvgIconView,
+							Href:   "/public" + pf_path + file_name + "/",
+							Hint:   "View",
+							Target: "_blank",
+						},
+						{
+							Icon:    assets.SysSvgIconRemove,
+							Href:    "javascript:fave.FilesManagerRemoveFolder(\\'" + pf_path + file_name + "\\',\\'Are you sure want to delete folder?\\');",
+							Hint:    "Delete",
+							Classes: "delete",
+						},
+					})
+					str_dirs += `<tr class="dir"><td class="col_name"><a href="javascript:fave.FilesManagerLoadData(\'` + pf_path + file_name + `/` + `\');"><span class="text-dotted">` + html.EscapeString(file_name) + `</span></a></td><td class="col_type"><b>DIR</b></td><td class="col_action">` + actions + `</td></tr>`
+				} else {
+					actions := builder.DataTableAction(&[]builder.DataTableActionRow{
+						{
+							Icon:   assets.SysSvgIconView,
+							Href:   "/public" + pf_path + file_name,
+							Hint:   "View",
+							Target: "_blank",
+						},
+						{
+							Icon:    assets.SysSvgIconRemove,
+							Href:    "javascript:fave.FilesManagerRemoveFile(\\'" + pf_path + file_name + "\\',\\'Are you sure want to delete file?\\');",
+							Hint:    "Delete",
+							Classes: "delete",
+						},
+					})
+					str_files += `<tr class="file"><td class="col_name"><span class="text-dotted">` + html.EscapeString(file_name) + `</span></td><td class="col_type">` + utils.Int64ToStr(utils.GetFileSize(file)) + `</td><td class="col_action">` + actions + `</td></tr>`
+				}
+			}
+		}
+
+		if pf_path != "/" {
+			str_dirs = `<tr class="dir"><td class="col_name"><a href="javascript:fave.FilesManagerLoadDataUp(\'` + pf_path + `\');">..</a></td><td class="col_type">&nbsp;</td><td class="col_action">&nbsp;</td></tr>` + str_dirs
+		}
+
+		table := `<table class="table data-table table-striped table-bordered table-hover table_fm_files"><thead><tr><th class="col_name">File name</th><th class="col_type">Size</th><th class="col_action">Action</th></tr></thead><tbody>` + str_dirs + str_files + `</tbody></table>`
+		wrap.Write(`$('#sys-modal-files-manager .dialog-data').html('` + table + `');`)
+
+		// Enable buttons
+		wrap.Write(`fave.FilesManagerEnableDisableButtons(false);`)
+	})
+}

+ 53 - 0
engine/modules/module_files_act_mkdir.go

@@ -0,0 +1,53 @@
+package modules
+
+import (
+	"os"
+	"strings"
+
+	"golang-fave/engine/utils"
+	"golang-fave/engine/wrapper"
+)
+
+func (this *Modules) RegisterAction_FilesMkdir() *Action {
+	return this.newAction(AInfo{
+		Mount:     "files-mkdir",
+		WantAdmin: true,
+	}, func(wrap *wrapper.Wrapper) {
+		pf_path := utils.Trim(wrap.R.FormValue("path"))
+		pf_name := utils.Trim(wrap.R.FormValue("name"))
+
+		if pf_path == "" {
+			wrap.MsgError(`Please specify folder path`)
+			return
+		}
+
+		if pf_name == "" {
+			wrap.MsgError(`Please specify folder name`)
+			return
+		}
+
+		dirname := utils.SafeFilePath(pf_path + pf_name)
+		target := strings.Join([]string{wrap.DHtdocs, "public"}, string(os.PathSeparator)) + dirname
+		if err := os.Mkdir(target, 0755); err != nil {
+			emsg := err.Error()
+			i := strings.Index(emsg, ":")
+			if i != -1 {
+				emsg = emsg[i+1:]
+			}
+			wrap.MsgError(emsg)
+			return
+		}
+
+		path := "/"
+		i := strings.LastIndex(dirname, string(os.PathSeparator))
+		if i != -1 {
+			path = dirname[:i+1]
+		}
+
+		// Set path
+		wrap.Write(`$('#sys-modal-files-manager .dialog-path span').html('` + path + `');`)
+
+		// Refresh table
+		wrap.Write(`fave.FilesManagerLoadData('` + path + `');`)
+	})
+}

+ 36 - 0
engine/modules/module_files_act_remove_file.go

@@ -0,0 +1,36 @@
+package modules
+
+import (
+	"os"
+	"strings"
+
+	"golang-fave/engine/utils"
+	"golang-fave/engine/wrapper"
+)
+
+func (this *Modules) RegisterAction_FilesRemoveFile() *Action {
+	return this.newAction(AInfo{
+		Mount:     "files-remove-file",
+		WantAdmin: true,
+	}, func(wrap *wrapper.Wrapper) {
+		pf_file := utils.SafeFilePath(utils.Trim(wrap.R.FormValue("file")))
+
+		file := strings.Join([]string{wrap.DHtdocs, "public"}, string(os.PathSeparator)) + pf_file
+		if err := os.Remove(file); err != nil {
+			wrap.MsgError(err.Error())
+			return
+		}
+
+		path := "/"
+		i := strings.LastIndex(pf_file, string(os.PathSeparator))
+		if i != -1 {
+			path = pf_file[:i+1]
+		}
+
+		// Set path
+		wrap.Write(`$('#sys-modal-files-manager .dialog-path span').html('` + path + `');`)
+
+		// Refresh table
+		wrap.Write(`fave.FilesManagerLoadData('` + path + `');`)
+	})
+}

+ 36 - 0
engine/modules/module_files_act_remove_folder.go

@@ -0,0 +1,36 @@
+package modules
+
+import (
+	"os"
+	"strings"
+
+	"golang-fave/engine/utils"
+	"golang-fave/engine/wrapper"
+)
+
+func (this *Modules) RegisterAction_FilesRemoveFolder() *Action {
+	return this.newAction(AInfo{
+		Mount:     "files-remove-folder",
+		WantAdmin: true,
+	}, func(wrap *wrapper.Wrapper) {
+		pf_file := utils.SafeFilePath(utils.Trim(wrap.R.FormValue("file")))
+
+		file := strings.Join([]string{wrap.DHtdocs, "public"}, string(os.PathSeparator)) + pf_file
+		if err := os.RemoveAll(file); err != nil {
+			wrap.MsgError(err.Error())
+			return
+		}
+
+		path := "/"
+		i := strings.LastIndex(pf_file, string(os.PathSeparator))
+		if i != -1 {
+			path = pf_file[:i+1]
+		}
+
+		// Set path
+		wrap.Write(`fave.FilesManagerSetPath('` + path + `');`)
+
+		// Refresh table
+		wrap.Write(`fave.FilesManagerLoadData('` + path + `');`)
+	})
+}

+ 63 - 0
engine/modules/module_files_act_upload.go

@@ -0,0 +1,63 @@
+package modules
+
+import (
+	"io"
+	"os"
+	"strings"
+
+	"golang-fave/engine/utils"
+	"golang-fave/engine/wrapper"
+)
+
+func (this *Modules) RegisterAction_FilesUpload() *Action {
+	return this.newAction(AInfo{
+		Mount:     "files-upload",
+		WantAdmin: true,
+	}, func(wrap *wrapper.Wrapper) {
+		pf_count := utils.Trim(wrap.R.FormValue("count"))
+		pf_path := utils.Trim(wrap.R.FormValue("path"))
+
+		if !utils.IsNumeric(pf_count) {
+			wrap.MsgError(`Inner system error`)
+			return
+		}
+
+		pf_count_int := utils.StrToInt(pf_count)
+		if pf_count_int <= 0 {
+			wrap.MsgError(`Inner system error`)
+			return
+		}
+
+		if pf_path == "" {
+			wrap.MsgError(`Please specify files path`)
+			return
+		}
+
+		for i := 1; i <= pf_count_int; i++ {
+			post_field_name := "file_" + utils.IntToStr(i-1)
+			if file, handler, err := wrap.R.FormFile(post_field_name); err == nil {
+				if handler.Filename != "" {
+					filename := utils.SafeFilePath(pf_path + handler.Filename)
+					target := strings.Join([]string{wrap.DHtdocs, "public"}, string(os.PathSeparator)) + filename
+					if f, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE, 0666); err == nil {
+						io.Copy(f, file)
+						f.Close()
+					}
+				}
+				file.Close()
+			}
+		}
+
+		path := "/"
+		i := strings.LastIndex(pf_path, string(os.PathSeparator))
+		if i != -1 {
+			path = pf_path[:i+1]
+		}
+
+		// Set path
+		wrap.Write(`$('#sys-modal-files-manager .dialog-path span').html('` + path + `');`)
+
+		// Refresh table
+		wrap.Write(`fave.FilesManagerLoadData('` + path + `');`)
+	})
+}

+ 1 - 1
engine/modules/module_index_act_mysql_setup.go

@@ -560,7 +560,7 @@ func (this *Modules) RegisterAction_IndexMysqlSetup() *Action {
 		}
 		if _, err = tx.Exec(
 			wrap.R.Context(),
-			`INSERT INTO fave_settings (name, value) VALUES ('database_version', '000000022');`,
+			`INSERT INTO fave_settings (name, value) VALUES ('database_version', '000000023');`,
 		); err != nil {
 			tx.Rollback()
 			wrap.MsgError(err.Error())

+ 29 - 0
engine/utils/utils.go

@@ -148,6 +148,17 @@ func ExtractHostPort(host string, https bool) (string, string) {
 	return h, p
 }
 
+func GetFileSize(filename string) int64 {
+	if st, err := os.Stat(filename); !os.IsNotExist(err) {
+		if err == nil {
+			if !st.Mode().IsDir() {
+				return st.Size()
+			}
+		}
+	}
+	return 0
+}
+
 func GetAssetsUrl(filename string) string {
 	return "/" + filename + "?v=" + consts.ServerVersion
 }
@@ -535,3 +546,21 @@ func SMTPSend(host, port, user, pass, subject, msg string, receivers []string) e
 		[]byte(message),
 	)
 }
+
+func SafeFilePath(path string) string {
+	result := path
+
+	if reg, err := regexp.Compile("([/]+\\.\\.)|(\\.\\.[/]+)"); err == nil {
+		result = reg.ReplaceAllString(result, "/")
+	}
+
+	if reg, err := regexp.Compile("([/]+[\\.]+[/]+)"); err == nil {
+		result = reg.ReplaceAllString(result, "/")
+	}
+
+	if reg, err := regexp.Compile("([/]+)"); err == nil {
+		result = reg.ReplaceAllString(result, "/")
+	}
+
+	return result
+}

+ 9 - 0
engine/utils/utils_test.go

@@ -400,3 +400,12 @@ func TestFormatProductPrice(t *testing.T) {
 	Expect(t, FormatProductPrice(123.4567, 3, 2), "123.000")
 	Expect(t, FormatProductPrice(123.4567, 4, 2), "123.0000")
 }
+
+func TestSafeFilePath(t *testing.T) {
+	Expect(t, SafeFilePath("/test/file"), "/test/file")
+	Expect(t, SafeFilePath("/test/../file"), "/test/file")
+	Expect(t, SafeFilePath("../test/file"), "/test/file")
+	Expect(t, SafeFilePath("/test/file/.."), "/test/file/")
+	Expect(t, SafeFilePath("/test/file/./"), "/test/file/")
+	Expect(t, SafeFilePath("/test/./file"), "/test/file")
+}

+ 0 - 0
hosts/localhost/htdocs/public/.keep


+ 1 - 0
support/migrate/000000001.go

@@ -30,4 +30,5 @@ var Migrations = map[string]func(context.Context, *sqlw.DB, string) error{
 	"000000020": Migrate_000000020,
 	"000000021": Migrate_000000021,
 	"000000022": Migrate_000000022,
+	"000000023": Migrate_000000023,
 }

+ 16 - 0
support/migrate/000000023.go

@@ -0,0 +1,16 @@
+package migrate
+
+import (
+	"context"
+	"os"
+
+	"golang-fave/engine/sqlw"
+)
+
+func Migrate_000000023(ctx context.Context, db *sqlw.DB, host string) error {
+	if err := os.Mkdir(host+string(os.PathSeparator)+"/htdocs/public", 0755); err != nil {
+		return err
+	}
+
+	return nil
+}

Some files were not shown because too many files changed in this diff