Commit 33f6011d authored by Frank Bergmann's avatar Frank Bergmann

- First version of HTML5 file-storage:

  - Create data-source, tree-panel and controller
  - implemented download
parent ddcec6a3
-- /packages/intranet-rest-fs-openacs/sql/postgresql/intranet-rest-fs-openacs-create.sql
--
-- Copyright (c) 2018 ]project-open[
--
-- All rights reserved. Please check
-- http://www.project-open.com/license/ for details.
--
-- @author frank.bergmann@project-open.com
-----------------------------------------------------------
--
-- Install crypto extension in order to have sha1 available
create extension pgcrypto;
-----------------------------------------------------------
-- Plugin Components
--
-- List of Risks per project
SELECT im_component_plugin__new (
null, 'im_component_plugin', now(), null, null, null,
'Project HTML5 File-Storage', -- plugin_name
'intranet-rest-fs-openacs', -- package_name
'right', -- location
'/intranet/projects/view', -- page_url
null, -- view_name
300, -- sort_order
'im_rest_fs_component -object_id $project_id' -- component_tcl
);
update im_component_plugins
set title_tcl = 'lang::message::lookup "" intranet-rest-fs-openacs.Project_HTML5_File_Storage "Project HTML5 File-Storage"'
where plugin_name = 'Project HTML5 File-Storage';
-- /package/intranet-forum/sql/intranet-rest-fs-openacs-drop.sql
--
-- Copyright (c) 2003-2006 ]project-open[
--
-- All rights reserved. Please check
-- http://www.project-open.com/license/ for details.
--
-- @author frank.bergmann@project-open.com
-- Drop plugins and menus for the module
--
select im_component_plugin__del_module('intranet-rest-fs-openacs');
select im_menu__del_module('intranet-rest-fs-openacs');
-- Uninstall crypto extension
drop extension pgcrypto;
......@@ -21,3 +21,17 @@ ad_library {
# Components
# ---------------------------------------------------------------------
ad_proc -public im_rest_fs_component {
-object_id
} {
Returns a HTML 5 component to show project related files
} {
# Sencha check and permissions
if {![im_sencha_extjs_installed_p]} { return "" }
im_sencha_extjs_load_libraries
set params [list [list object_id $object_id]]
set result [ad_parse_template -params $params "/packages/intranet-rest-fs-openacs/lib/file-tree"]
return [string trim $result]
}
// /sencha-core/www/model/file/File.js
//
// Copyright (C) 2013 ]project-open[
//
// All rights reserved. Please see
// http://www.project-open.com/license/ for details.
/**
A "File" represents a content item in the database.
Files are stored in content folders.
Content folders can be associated with a project via
acs_rel.
**/
Ext.define('FileStorage.File', {
extend: 'Ext.data.Model',
xtype: 'file',
fields: [
// Identity
'id', // The system wide unique ID for this object (-> cr_items.item_id)
'name', // The name of the task (inherited from File)
'version_id', // The active version of the file (-> cr_revisions.revision_id)
'description', // Description of the task activity
'title', // title
'iconCls', // Used by ExtJS for the icon, seems to work
'content_length', //
'sha1', // sha-1 Hash of the content
'expanded', // true or false (without quotes), default state for tree
'type', // folder or mime-type
'leaf', // leaf or has sub-elements?
{ name: 'icon', // A   sequence representing the file indentation
convert: function(value, record) {
var type = record.get('type');
console.log('PO.model.file.File.icon: Type='+type);
switch (type) {
case 101: return '/intranet/images/navbar_default/tag_blue.png'; break; // Ticket
case 2502: return '/intranet/images/navbar_default/table.png'; break; // SLA
default: return ''; // Empty string - enables default behavior
}
}
},
{ name: 'indent', // A   sequence representing the file indentation
convert: function(value, record) {
var level = record.get('level');
var result = '';
while (level > 0) {
result = '       ' + result;
level = level - 1;
}
return result;
}
}
]
});
/*
* FileTreePanel.js
*
* Copyright (c) 2011 - 2018 ]project-open[ Business Solutions, S.L.
* This file may be used under the terms of the GNU General Public
* License version 3.0 or alternatively unter the terms of the ]po[
* FL or CL license as specified in www.project-open.com/en/license.
*/
/**
* TreePanel with the list of files.
*/
Ext.define('FileStorage.FileTreePanel', {
extend: 'Ext.tree.Panel',
alias: 'fileTreePanel',
animate: false, // Animation messes up bars on the right side
collapsible: false,
multiSelect: true,
rootVisible: false,
singleExpand: false,
shrinkWrap: false,
title: false,
useArrows: true,
// Scrolling
overflowX: 'scroll', // Allows for horizontal scrolling, but not vertical...
scrollFlags: {x: true}, // ... vertical scrolling is handled by the FileTree
// the 'columns' property is now 'headers'
columns: [
{text: 'Download',
xtype: 'actioncolumn',
dataIndex: 'id',
width: 50,
items: [{
icon: '/intranet/images/external.png',
tooltip: 'Link',
handler: function(grid, rowIndex, colIndex) {
var rec = grid.getStore().getAt(rowIndex);
var url = '/intranet-rest-fs-openacs/download?file_id='+rec.get('id');
window.open(url); // Open project in new browser tab
}
}]
},
{text: 'Description', stateId: 'treegrid-description', flex: 1, hidden: true, dataIndex: 'description'},
{text: 'File', stateId: 'treegrid-File', xtype: 'treecolumn', flex: 2, sortable: false, dataIndex: 'name',
editor: true,
renderer: function(v, context, model, d, e) {
context.style = 'cursor: pointer;';
var children = model.childNodes;
if (0 == children.length) { return model.get('name'); } else { return "<b>"+model.get('name')+"</b>"; }
}},
{text: 'Description', stateId: 'treegrid-description', flex: 1, hidden: true, dataIndex: 'description', editor: {allowBlank: true}}
],
initComponent: function() {
var me = this;
if (me.debug) console.log('FileStorage.FileTreePanel.initComponent: Starting');
this.callParent(arguments);
if (me.debug) console.log('FileStorage.FileTreePanel.initComponent: Finished');
}
});
/*
* FileTreePanelController.js
*
* Copyright (c) 2011 - 2018 ]project-open[ Business Solutions, S.L.
* This file may be used under the terms of the GNU General Public
* License version 3.0 or alternatively unter the terms of the ]po[
* FL or CL license as specified in www.file-open.com/en/license.
*/
/**
* Deal with collapsible tree nodes, keyboard commands
* and the interaction with the FileBarPanel.
*/
Ext.define('FileStorage.FileTreePanelController', {
extend: 'Ext.app.Controller',
requires: ['Ext.app.Controller'],
refs: [
{ref: 'fileBarPanel', selector: '#fileBarPanel'},
{ref: 'fileTreePanel', selector: '#fileTreePanel'}
],
fileTreePanel: null,
init: function() {
var me = this;
this.control({
'#fileTreePanel': {
'itemcollapse': this.onItemCollapse,
'itemexpand': this.onItemExpand
},
'#buttonDelete': { click: this.onButtonDelete}
});
},
/**
* The user has collapsed a file in the FileTreePanel.
* We now save the 'c'=closed status using a ]po[ URL.
* These values will appear in the FileTreeStore.
*/
onItemCollapse: function(fileModel) {
var me = this;
var object_id = fileModel.get('id');
Ext.Ajax.request({
url: '/intranet/biz-object-tree-open-close.tcl',
params: { 'object_id': object_id, 'open_p': 'c' }
});
},
/**
* The user has expanded a super-file in the FileTreePanel.
* Please see onItemCollapse for further documentation.
*/
onItemExpand: function(fileModel) {
var me = this;
if (me.debug) console.log('PO.class.FileDrawComponent.onItemExpand: ');
// Remember the new state
var object_id = fileModel.get('id');
Ext.Ajax.request({
url: '/intranet/biz-object-tree-open-close.tcl',
params: { 'object_id': object_id, 'open_p': 'o' }
});
},
/**
* "Delete" (-) button pressed.
* Delete the currently selected file from the tree.
*/
onButtonDelete: function() {
var me = this;
if (me.debug) console.log('PO.view.file.FileTreePanelController.onButtonDelete: ');
// ToDo: delete file
}
});
// /sencha-core/www/store/timesheet/FileTreeStore.js
//
// Copyright (C) 2013-2018 ]project-open[
//
// All rights reserved. Please see
// http://www.project-open.com/license/ for details.
/**
* Stores all files of a single project as a hierarchical tree.
* The store is used by the ]po[ Gantt Editor and the list of
* files per project.
*/
Ext.define('FileStorage.FileTreeStore', {
extend: 'Ext.data.TreeStore',
storeId: 'fileTreeStore',
model: 'FileStorage.File',
autoload: false,
autoSync: false, // We need manual control for saving etc.
folderSort: false, // Needs to be false in order to preserve the MS-Project import order
proxy: {
type: 'ajax',
url: '/intranet-rest-fs-openacs/file-tree.json',
extraParams: {
project_id: 0 // Will be set by app before loading
},
api: {
read: '/intranet-rest-fs-openacs/file-tree.json?read=1',
create: '/intranet-rest-fs-openacs/file-tree-action?action=create',
update: '/intranet-rest-fs-openacs/file-tree-action?action=update',
destroy: '/intranet-rest-fs-openacs/file-tree-action?action=delete'
},
reader: {
type: 'json',
rootProperty: 'data'
},
writer: {
type: 'json',
rootProperty: 'data'
}
},
/**
* Returns an entry for a file_id
*/
getById: function(file_id) {
var rootNode = this.getRootNode();
var resultModel = null;
rootNode.cascadeBy(function(model) {
var id = model.get('id');
if (file_id == id) {
resultModel = model;
}
});
return resultModel;
},
/**
* Return an array with the tree items ordered by sort_order.
* The resulting array should not have "holes".
*/
getSortOrderArray: function() {
var me = this;
var result = new Array();
var rootNode = me.getRootNode();
// Iterate through all children of the root node
rootNode.cascadeBy(function(model) {
var sort_order = +model.get('sort_order');
if (0 != sort_order) {
result[sort_order] = model;
}
});
return result;
}
});
# /packages/intranet-rest-fs-openacs/www/download.tcl
#
# Copyright (C) 2018 ]project-open[
ad_page_contract {
Return a file from the content-repository.
You can specify either the file_id or the version_id (a specific version of the file).
@author Frank Bergmann
@cvs-id $Id$
} {
{ file_id "" }
{ version_id "" }
}
if {"" eq $file_id && "" ne $version_id} {
set file_id [db_string cr_item "select item_id from cr_revisions where revision_id = :version_id" -default ""]
}
# Get the paths
set revision_id $version_id
set the_root $::acs::pageroot
set the_url [ad_conn path_info]
set content_type "content_revision"
set content_root [fs::get_root_folder]
set template_root [db_string template_root "select content_template__get_root_folder()"]
set user_id [ad_conn user_id]
# Serve the page
# DRB: Note that content::init modifies the local variable the_root, which is treated
# as though it's been passed by reference. This requires that the redirect treat the
# path as an absolute path within the filesystem.
if {[parameter::get -parameter BehaveLikeFilesystemP -default 0] || [catch {set init_p [content::init the_url the_root $content_root $template_root public $revision_id $content_type]}] || !$init_p } {
# Make sure we are not dealing with an upgraded file and there exists a file with the title
if {$file_id eq ""} {
set splitted_url [split $the_url "/"]
set item_url_title [lindex $splitted_url end]
# THIS CODE ONLY TAKES TWO FOLDERS INTO ACCOUNT. THIS NEEDS TO BE FIXED LATER
set item_url_folder [lindex $splitted_url end-1]
set item_url_parent_folder [lindex $splitted_url end-2]
set file_id [db_string upgraded_item_id {} -default 0]
}
if {$file_id == 0} {
ns_returnnotfound
} else {
if {[content::symlink::is_symlink -item_id $file_id]} {
set file_id [content::symlink::resolve -item_id $file_id]
}
if {(![info exists version_id] || $version_id eq "")} {
set version_id [content::item::get_live_revision -item_id $file_id]
}
if {[apm_package_installed_p views]} {
views::record_view -object_id $file_id -viewer_id $user_id
}
permission::require_permission \
-party_id $user_id \
-object_id $version_id \
-privilege read
cr_write_content -revision_id $version_id
}
} else {
set version_id [content::item::get_live_revision -item_id $file_id]
permission::require_permission \
-party_id $user_id \
-object_id $version_id \
-privilege read
set file "$the_root/$the_url"
rp_internal_redirect -absolute_path $file
}
{'text':'.','children': [
@file_json;noquote@
}
]}
# /packages/intranet-rest-fs-openacs/www/file-tree.json.tcl
#
# Copyright (C) 2013-2018 ]project-open[
ad_page_contract {
Returns a JSON tree structure suitable for batch-loading a project TreeStore
@param object_id ]po[ Business Object to which the FS belongs.
0 identifies the global "Home" FS
@author frank.bergmann@project-open.com
@param node Passed by ExtJS to load sub-trees of a tree.
Normally not used, just in case of error.
} {
{object_id:integer 0}
{debug_p 0}
{node ""}
}
set root_file_id $object_id
if {"" ne $node && [string is integer $node]} { set root_file_id $node }
# --------------------------------------------
# Security & Permissions
#
set current_user_id [auth::require_login]
set ttt {
im_object_permissions $current_user_id $object_id view read write admin
if {!$read} {
im_rest_error -format "json" -http_status 403 -message "You (user #$current_user_id) have no permissions to read object #$object_id"
ad_script_abort
}
}
set folder_id [fs_get_root_folder]
set folder_id 36012
# permission::require_permission -party_id $current_user_id -object_id $folder_id -privilege "read"
# set package_and_root [fs::get_folder_package_and_root $folder_id]
# set package_id [lindex $package_and_root 0]
# set root_folder_id [lindex $package_and_root 1]
set folder_path ""
# --------------------------------------------
# Main hierarchical SQL
#
set top_level [db_string top_level "select tree_level(tree_sortkey) from cr_items where item_id = :folder_id"]
set sql "
select
ci.item_id as file_id,
ci.name as file_name,
cr.revision_id as version_id,
tree_level(ci.tree_sortkey) - :top_level as level,
encode(digest(cr.content, 'sha1'), 'hex') as sha1,
(select count(*) from cr_items child where child.parent_id = ci.item_id) as num_children,
ci.*,
cr.*,
CASE WHEN bts.open_p = 'o' THEN 'true' ELSE 'false' END as expanded,
CASE
WHEN ci.content_type::text = 'content_folder'::text THEN 'folder'::character varying
WHEN ci.content_type::text = 'content_extlink'::text THEN 'url'::character varying
WHEN ci.content_type::text = 'content_symlink'::text THEN 'symlink'::character varying
ELSE cr.mime_type
END AS type
from cr_items parent_ci,
cr_items ci
LEFT JOIN cr_extlinks ce ON ci.item_id = ce.extlink_id
LEFT JOIN cr_folders cf ON ci.item_id = cf.folder_id
LEFT JOIN cr_revisions cr ON ci.live_revision = cr.revision_id
LEFT JOIN cr_mime_types cm ON cr.mime_type = cm.mime_type
JOIN acs_objects o ON ci.item_id = o.object_id
LEFT OUTER JOIN im_biz_object_tree_status bts ON (
ci.item_id = bts.object_id and
bts.page_url = 'default' and
bts.user_id = :current_user_id
)
where
parent_ci.item_id = :folder_id and
ci.tree_sortkey between parent_ci.tree_sortkey and tree_right(parent_ci.tree_sortkey)
-- and tree_level(ci.tree_sortkey) < 4
order by
ci.tree_sortkey
"
# ad_return_complaint 1 [im_ad_hoc_query -subtotals_p 0 -format html $sql]
# Read the query into a Multirow, so that we can order
# it according to sort_order within the individual sub-levels.
db_multirow file_multirow file_list $sql {
# By default open the top-level file-storage
if {0 eq $level} { set expanded "true" }
}
# --------------------------------------------
# Get all the extra variables for a file ci_item:
# fraber 2018-08-22: No meta-variables yet...
set valid_vars [list level version_id type publish_date mime_type content_length sha1 title description]
set valid_vars [lsort -unique $valid_vars]
set file_json ""
set ctr 0
set old_level 0
set indent ""
template::multirow foreach file_multirow {
ns_log Notice "file-tree.json.tcl: file_id=$file_id, file_id=$file_id"
if {$debug_p} { append file_json "\n// finish: ctr=$ctr, level=$level, old_level=$old_level\n" }
# -----------------------------------------
# Close off the previous entry
# -----------------------------------------
# This is the first child of the previous item
# Increasing the level always happens in steps of 1
if {$level > $old_level} {
append file_json ",\n${indent}\tchildren:\[\n"
}
# A group of children needs to be closed.
# Please note that this can cascade down to several levels.
while {$level < $old_level} {
append file_json "\n${indent}\}\]\n"
incr old_level -1
set indent ""
for {set i 0} {$i < $old_level} {incr i} { append indent "\t" }
}
# The current file is on the same level as the previous.
# This is also executed after reducing the old_level in the previous while loop
if {$level == $old_level} {
if {0 != $ctr} {
append file_json "${indent}\n${indent}\},\n"
}
}
if {$debug_p} { append file_json "\n// $file_name: ctr=$ctr, level=$level, old_level=$old_level\n" }
set indent ""
for {set i 0} {$i < $level} {incr i} { append indent "\t" }
if {0 == $num_children} { set leaf_json "true" } else { set leaf_json "false" }
set quoted_char_map {"\n" "\\n" "\r" "\\r" "\"" "\\\"" "\\" "\\\\"}
set quoted_file_name [string map $quoted_char_map $file_name]
append file_json "${indent}\{
${indent}\tid:$file_id,
${indent}\tname:\"$quoted_file_name\",
${indent}\ticonCls:\"icon-$type\",
${indent}\texpanded:$expanded,
"
foreach var $valid_vars {
# Skip xml_* variables (only used by MS-File)
if {[regexp {^xml_} $var match]} { continue }
# Append the value to the JSON output
set value [set $var]
set quoted_value [string map $quoted_char_map $value]
append file_json "${indent}\t$var:\"$quoted_value\",\n"
}
append file_json "${indent}\tleaf:$leaf_json"
incr ctr
set old_level $level
}
set level 0
while {$level < $old_level} {
# A group of children needs to be closed.
# Please note that this can cascade down to several levels.
append file_json "\n${indent}\}\]\n"
incr old_level -1
set indent ""
for {set i 0} {$i < $old_level} {incr i} { append indent "\t" }
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment