EZDownload Module and MySQL
I am trying to use the EZDownload module that I just saw posted by openmfg. However, the script is only for postgres and I can not get it to work on MySQL 5. MYSQL complains of errors in the script on these following lines:
ALTER TABLE ONLY leaf
ADD CONSTRAINT leaf_pkey PRIMARY KEY (nid);
INSERT into leaf(nid, pid, leaftype, attachment) VALUES (nextval('node_nid_seq'), -1, 'd', '');
INSERT into node(nid, "type", title, uid, status, created, changed, "comment", promote, moderate, teaser, body, revisions, sticky, format) VALUES (currval('node_nid_seq'), 'ezdown', 'Root DIR', 1, 1, 1149089816, 1149629236, 0, 0, 0, '','','', 0, 0);
The first one I can fix, but using the proper primary key syntax for MYSQL.
The next two though are more difficult. Basically, MYSQL is complaining that the functions nextval and currval dont exist, and also that the 'node_nid_seq' items do not exist either.
Anyone gotten this to work on MySQL that can share the MYSQL Script? If I get it to work, I will gladly post it and submit it to the module dev team.

Thanks for checking out module out ...
We had a very similar question about MySQL support. Take a look at this. It discusses our future plans for EZdownload. I will probably be posting a MySQL data base for this, as it seems that many of Drupal modules do not support PostgreSQL, (I think I might be on of the few using it as a DB).
--Trevor
MySQL for EZ Download
Trevor -
THANK YOU! for working on the MySQL for EZD. This module is EXACTLY what I need for my intranet site. Do you have an idea of when this would be released? I need to put the site on-hold until I can get something like this available. I would ne HAPPY to beta this for you ...
Thanks again -
Chris
I think I might have spoke too soon
I do have to note that will be something I have to do on my own time (i.e at home on my own computer in the dark at 3:00 in the morning :-) ... Where I work postgres is the standard so it wouldn't do them a lot of good for me to convert this to MySQL. I do understand that a lot of people use MySql with Drupal, just because that is the popular database for web developement. I wouldn't however suggest it is superior. Postgres Has been around a lot longer then MySQL and has the benifits of procedural a language and flows the latest ANSI SQL standards. MySQL in comparison will do some funky things if proper precautions are not made. Inserting Strings as a list of digits, truncating data, corupting date types, etc. I definitly don't want to start any type of database debate ... but I have worked with both of them and I prefer postgres.
In the end i really need to set some feelers out before I can make a firm commitment on the MySQL db. There might be a bigger picture here I am missing.
What it's complaining about ...
It's Complaining about the sequences and thier functions not being in place. sequences are something MySQL hasn't implemented. currval and nextval are functions that operate on a sequence. Node_id_seq is a name of the seqence for the nid column of the node table. I think drupal for Mysql actually has a sequence table that it pulls from (not sure you would have to check this)... if not that the table might be auto_incremented ..... ... instead sequences MySQL uses auto_increment statements on columns (which is not SQL standard :-( ..........
--Trevor
what about this?
CREATE TABLE leaf (
nid integer NOT NULL,
pid integer,
leaftype character(1),
attachment text
);
ALTER TABLE `leaf` ADD PRIMARY KEY ( `nid` );
ALTER TABLE `leaf` CHANGE `nid` `nid` INT( 11 ) NOT NULL AUTO_INCREMENT;
INSERT into leaf(nid, pid, leaftype, attachment) VALUES ('', -1, 'd', '');
INSERT into node(nid, `type`, title, uid, `status`, created, `changed`, `comment`, promote, moderate, sticky) VALUES (0, 'ezdown', 'Root DIR', 1, 1, 1149089816, 1149629236, 0, 0, 0, 0);
still doesn't work though because 4.7 is different from 4.6
and i don't know enough about drupal yet tho know in which ways it differs
:(
Almost ...
First I would like to apologize for the delay on the mysql database ... I had to talk with my boss about it and I had a little down time in terms of internet at my home. My Internet is now back up and I should have a mysql db in the near future.
In terms of the table above ... almost ... but not quite ...
This part is fine ...
CREATE TABLE leaf (nid integer NOT NULL,
pid integer,
leaftype character(1),
attachment text
);
This part needs a little work ....
// You can do this line if you really want ... but it's not nessecary and not really correct ...
// nid is a foriegn key ... but I wouldn't suggest puting it in there as such ...
// I would get rid of this line
ALTER TABLE `leaf` ADD PRIMARY KEY ( `nid` );
// You don't want to do this ... the nid column is a implied foriegn key to the node table. ... get rid of this line too
ALTER TABLE `leaf` CHANGE `nid` `nid` INT( 11 ) NOT NULL AUTO_INCREMENT;
// Change the 0 to NULL ... this is to get the next auto increment number from nodes ...
INSERT into node(nid, `type`, title, uid, `status`, created, `changed`, `comment`, promote, moderate, sticky) VALUES (NULL, 'ezdown', 'Root DIR', 1, 1, 1149089816, 1149629236, 0, 0, 0, 0);
// Use the LAST_INSERT_ID() function here .....
INSERT into leaf(nid, pid, leaftype, attachment) VALUES ( LAST_INSERT_ID() , -1, 'd', '');
So your final script should look like .....
CREATE TABLE leaf (
nid integer NOT NULL,
pid integer,
leaftype character(1),
attachment text
);
INSERT into node(nid, `type`, title, uid, `status`, created, `changed`, `comment`, promote, moderate, sticky) VALUES (NULL, 'ezdown', 'Root DIR', 1, 1, 1149089816, 1149629236, 0, 0, 0, 0);
INSERT into leaf(nid, pid, leaftype, attachment) VALUES ( LAST_INSERT_ID() , -1, 'd', '');
Give this a shoot and let me know .... I will try testing it myself tonight .....
--Trevor
added utf8 support and revisions
CREATE TABLE leaf (
nid integer NOT NULL,
pid integer,
leaftype character(1),
attachment text
) TYPE=MyISAM /*!40100 DEFAULT CHARACTER SET utf8 */;
ALTER TABLE `leaf` ADD PRIMARY KEY ( `nid` );
INSERT into node(nid, `type`, title, uid, `status`, created, `changed`, `comment`, promote, moderate, sticky)
VALUES (NULL, 'ezdown', 'Root DIR', 1, 1, 1149089816, 1149629236, 0, 0, 0, 0);
UPDATE `node` SET `vid` = LAST_INSERT_ID() WHERE `nid` = LAST_INSERT_ID() LIMIT 1;
INSERT INTO `node_revisions` ( `nid` , `vid` , `uid` , `title` , `body` , `teaser` , `log` , `timestamp` , `format` )
VALUES ( LAST_INSERT_ID(), LAST_INSERT_ID(), '0', 'Root DIR', 'EZ Download Root Directory', '', '', '1149089816', '0' );
INSERT into leaf(nid, pid, leaftype, attachment) VALUES ( LAST_INSERT_ID() , -1, 'd', '');
The node_revisions insert was required to make the node_load() function return anything
thus fixing _ezdown_find_root() and consequently ezdown_page()
I am still working on testing the 4.7 port
I'll post it here as soon as I think it's usable
Note:
you need the primary key
otherwise weird things happen - php runs out of memory if a duplicate nid somehow gets entered into the leaf table
I have hit a bbit of a roadblock :(
when I try to add the first leaf,
it fails because it is trying to use the same nid as the install did
auto-increment seems to not work properly :(
and I can't figure out why :(
i am starting to think that it would be better to just create the table
and move the rest to a ezdown.install file
i'll let you know how it goes
ezdown.install & ezdown.module (4.7/MySQL)
ezdown.install
<?php
/**
* Implementation of hook_install().
*/
function ezdown_install() {
switch ($GLOBALS['db_type']) {
case 'mysql':
case 'mysqli':
db_query("CREATE TABLE {leaf} (
nid integer NOT NULL,
parentid integer,
leaftype character(1),
attachment text,
PRIMARY KEY (nid)
) TYPE=MyISAM /*!40100 DEFAULT CHARACTER SET utf8 */;");
global $user;
$node = new StdClass();
$node->type = 'ezdown';
$node->uid = $user->uid;
$node->status = 1;
$node->promote = 0;
$node->sticky = 0;
$node->title = 'All Downloads';
node_save($node);
$nid = $node->nid;
db_query("INSERT INTO {leaf} (nid, parentid, leaftype)
VALUES (%d, %d, '%s')", $nid, -1, 'd');
break;
case 'pgsql':
db_query("CREATE TABLE {leaf} (
nid integer NOT NULL,
parentid integer,
leaftype character(1),
attachment text
);");
db_query("ALTER TABLE ONLY {leaf}
ADD CONSTRAINT leaf_pkey
PRIMARY KEY (nid);");
db_query("INSERT into {leaf}(nid, parentid, leaftype, attachment)
VALUES (nextval('node_nid_seq'), -1, 'd', '');");
db_query("INSERT into {node} (
nid, `type`, title, uid, status, created, changed, `comment`,
promote, moderate, teaser, body, revisions, sticky, format)
VALUES (currval('node_nid_seq'), 'ezdown', 'Root DIR', 1, 1,
1149089816, 1149629236, 0, 0, 0, '','','', 0, 0); ");
break;
}
}
function ezdown_update_1() {
return _system_update_utf8(array('leaf'));
}
function ezdown_update_2() {
return db_query("ALTER TABLE `leaf` CHANGE `pid` `parentid` INT( 11 ) NULL DEFAULT NULL;");
}
?>
ezdown.module
<?php
// $Id: EZdownload Modules version 0.5
/** ---------------------------- Drupal Hooks ---------------------------- */
/**
* Implementation of hook_help().
*/
function ezdown_help($section) {
switch ($section) {
case 'admin/modules#description':
// This description is shown in the listing at admin/modules.
return t('A download module for use with role based permissions.');
case 'node/add#ezdown':
// This description shows up when users click "create content."
return t('Create an EZdownload leaf node.
These leaf nodes can be classified as a file or a direcotry
and are organized in a tree like structure.');
}
}
/**
* Implementation of hook_node_info().
*/
function ezdown_node_info() {
return array('ezdown' => array('name' => t('download'), 'base' => 'ezdown'));
}
/**
* Implementation of hook_access().
*/
function ezdown_access($op, $node) {
global $user;
if ($op == "create" || $op == "delete" || $op == "update") {
return user_access('administer ezdown');
}
elseif ($op == 'view') {
return user_access('access content');
}
else {
return false;
}
}
/**
* Implementation of hook_perm().
*/
function ezdown_perm() {
return array('access content', 'administer ezdown');
}
/**
* Implementation of hook_menu().
*/
function ezdown_menu($may_cache) {
drupal_set_html_head(theme('stylesheet_import', '/modules/ezdownload/ezdownload.css'));
$items = array();
if ($may_cache) {
$items[] = array('path' => 'downloads',
'title' => t('downloads'),
'callback' => 'ezdown_page',
'access' => user_access('access content'),
'type' => MENU_NORMAL_ITEM);
$items[] = array('path' => 'node/add/ezdown', 'title' => t('download'),
'access' => user_access('administer ezdown'));
}
return $items;
}
/**
* Implementation of hook_view().
*/
function ezdown_view(&$node, $teaser = FALSE, $page = FALSE) {
drupal_set_html_head(theme('stylesheet_import', '/modules/ezdownload/ezdownload.css'));
$node->body .= _ezdown_build_links($node->nid);
$crumbs = _ezdown_build_crumbs($node);
menu_set_location($crumbs);
if (user_access('administer ezdown') && $node->leaftype == 'd') {
$node->body .= l('Create a new file or subfolder',
'node/add/ezdown',
$attributes = array(),
$query = NULL,
$fragment = NULL,
$absolute = FALSE,
$html = FALSE );
}
$node = node_prepare($node, $teaser);
}
/**
* Implementation of hook_form().
*/
function ezdown_form(&$node, &$param) {
$form['title'] = array(
'#type' => 'textfield',
'#title' => t('Subject'),
'#default_value' => $node->title,
'#size' => 60,
'#maxlength' => 128,
'#required' => TRUE
);
$form['body_filter']['body'] = array(
'#type' => 'textarea',
'#title' => t('Description'),
'#default_value' => $node->body,
'#rows' => 5,
'#required' => TRUE
);
$form['body_filter']['format'] = filter_form($node->format);
$root = _ezdown_find_root();
if ($node->nid != $root->nid) {
// editing non-root node
if ($node->leaftype) $default_leaftype = $node->leaftype;
else $default_leaftype = 'd';
$form['leaftype'] = array(
'#type' => 'radios',
'#title' => t('Type of Leaf'),
'#default_value' => $default_leaftype,
'#options' => array( 'd' => "Subfolder", 'f' => "File" ),
'#description' => t("Please choose the type of node you wish to create"),
'#required' => TRUE,
'#attributes' => NULL
);
}
else {
//editing root node
$form['leaftype'] = array(
'#type' => 'hidden',
'#value' => 'd'
);
}
if ($node->nid != $root->nid) {
// editing non-root node
$parents = _ezdown_build_select();
$form['parentid'] = array(
'#type' => 'select',
'#title' => t('Parent'),
'#default_value' => $node->parentid,
'#options' => $parents,
'#description' => t('Please select the parent of the new download or subfolder'),
'#extra' => 0,
'#multiple' => false,
'#required' => true
);
}
else {
//editing root node
$form['parentid'] = array(
'#type' => 'hidden',
'#value' => $node->parentid
);
}
return $form;
}
/**
* Implementation of hook_nodeapi().
*/
function ezdown_nodeapi(&$node, $op, $teaser = NULL, $page = NULL ) {
if ($node->type == 'ezdown') {
$nobj = (array)$node;
$nobj = implode($nobj, ', ');
switch($op) {
case "validate":
if (!empty($node->nid)) {
if ((count($node->files) > 1) && $node->leaftype == 'f') {
form_set_error('upload', 'A download node may only have one attachment');
}
/*
if (($node->leaftype == 'd') && (!empty($node->files))) {
form_set_error('leaftype', 'A directory can not have a file attached');
}*/
break;
}
}
}
}
/**
* Implementation of hook_insert().
*
* As a new node is being inserted into the database, we need to do our own
* database inserts.
*/
function ezdown_insert($node) {
if ($node->nid && $node->parentid && $node->leaftype) {
//if parent is not specified then it's probably the install script and we want to skip this isnert
db_query("INSERT INTO {leaf} (nid, parentid, leaftype)
VALUES (%d, %d, '%s')",
$node->nid, $node->parentid, $node->leaftype);
}
}
/**
* Implementation of hook_update().
*
* As an existing node is being updated in the database, we need to do our own
* database updates.
*/
function ezdown_update($node) {
if ($node->nid && $node->parentid && $node->leaftype) {
db_query("UPDATE {leaf}
SET
`parentid` = '" .$node->parentid. "',
`leaftype` = '" .$node->leaftype. "',
`attachment` = NULL
WHERE
`nid` =" .$node->nid. "
LIMIT 1;");
}
}
/**
* Implementation of hook_delete().
*
* When a node is deleted, we need to clean up related tables.
*/
function ezdown_delete($node) {
db_query('DELETE FROM {leaf} WHERE nid = %d', $node->nid);
//db_query('delete from {leaf} where parentid not in (select nid from {leaf}) and parentid != -1');
}
/**
* Implementation of hook_load().
*
* Now that we've defined how to manage the node data in the database, we
* need to tell Drupal how to get the node back out. This hook is called
* every time a node is loaded, and allows us to do some loading of our own.
*/
function ezdown_load($node) {
$leaf = db_fetch_object(db_query('SELECT * FROM {leaf} WHERE nid = %d', $node->nid));
return $leaf;
}
/** ---------------------------- ezdown_menu() helper function ---------------------------- */
/**
* Show default Downloads page
* - called from ezdown_menu().
*/
function ezdown_page() {
$node = _ezdown_find_root();
drupal_goto('node/' . $node->nid);
}
/** ------------------- ezdown_menu() and ezdown_form() helper function ------------------- */
/**
* Returns the root leaf node object
*
* - called from ezdown_page().
* - which is called from ezdown_menu().
*
* - ALSO called from _ezdown_build_select()
* - which is called from ezdown_form().
*
* - ALSO directly called from ezdown_form().
*/
function _ezdown_find_root() {
$result = db_query('SELECT nid from {leaf} where parentid = -1');
if (db_num_rows($result) == 1) {
$node = db_fetch_object($result);
return node_load($node->nid, $revision = NULL, $reset = NULL);
}
else {
return false;
}
}
/** ---------------------------- ezdown_form() helper functions ---------------------------- */
/**
* - called from ezdown_form().
*/
function _ezdown_build_select() {
$leaves = _ezdown_build_path();
/** Process array for form. Get rid of $leaf['parentid'] and $leaf['nid'] (leaves are indexed by nid**/
$leaf_title_temp = "";
foreach($leaves as $leaf) {
$leaf_title_temp = $leaf['path'];
$leaves[$leaf['nid']] = $leaf_title_temp;
}
/**Need to exclude leaves that user can't view **/
return $leaves;
}
/**
* - called from _ezdown_build_select()
* - which is called from ezdown_form().
*/
function _ezdown_build_path() {
$leaves = _ezdown_build_tree();
$glue = '/';
/** Let's put the slash on root first **/
$root = _ezdown_find_root();
$leaves[$root->nid]['title'] = $glue . $root->title;
/** For each $leaf find the absolute path name and assign it to the title attribute. Using parentid as a pointer **/
foreach($leaves as $leaf) {
while($leaf['pointer'] != -1) {
$leaf['path'] = $leaves[$leaf['pointer']]['title'] . $glue . $leaf['path'];
/** advance pointer to grand parent **/
$leaf['pointer'] = $leaves[$leaf['pointer']]['parentid'];
$leaves[$leaf['nid']] = $leaf;
}
}
return $leaves;
}
/**
* - called from _ezdown_build_path()
* - which is called from _ezdown_build_select()
* - which is called from ezdown_form().
*/
function _ezdown_build_tree() {
$query = "SELECT e.nid, n.title, n.title as path, e.parentid, e.parentid as pointer
FROM leaf e, node n
WHERE leaftype='d'
AND e.nid = n.nid";
$leaves = array();
$result = db_query($query);
/**make our array of leaves indexed by nid**/
while($leaf = db_fetch_array($result)) {
$leaves[$leaf['nid']] = $leaf;
}
return $leaves;
}
/** ---------------------------- ezdown_view() helper functions ---------------------------- */
/**
* - called from ezdown_view().
*/
function _ezdown_build_crumbs($node) {
$breadcrumb = array();
$nid_pointer = $node->parentid;
while($p_node = _ezdown_find_parent($nid_pointer)) {
$breadcrumb[] = array('path' => 'node/'. $p_node->nid, 'title' => '/' . $p_node->title);
$nid_pointer = $p_node->parentid;
}
$breadcrumb = array_reverse ($breadcrumb, TRUE );
$breadcrumb[] = array('path' => 'node/'. $node->nid, 'title' => '/' . $node->title);
return $breadcrumb;
}
/**
* Returns the parent leaf node object
* - called from _ezdown_build_crumbs()
* - which is called from ezdown_view().
*/
function _ezdown_find_parent($leaf_id) {
$result = db_query('select * from {leaf} where nid = %d', $leaf_id);
if (db_num_rows($result) == 1) {
return node_load($leaf_id, $revision = NULL, $reset = NULL);
}
else {
return FALSE;
}
}
/**
* - called from ezdown_view().
*/
function _ezdown_build_links($root_id) {
$children = _ezdown_find_childern($root_id);
if (empty($childern)) {
$output .= "<ul style>";
foreach($children as $child) {
$link = l($child->title, "node/" . $child->nid);
if ($child->leaftype == 'd') {
$link = "<li class='ezfolder'>" . $link . "</li>";
}
elseif ($child->leaftype == 'f') {
$link = "<li class='ezfile'>" . $link . "</li>";
}
$output .= $link;
$output .= _ezdown_build_links($child->nid);
}
$output .= "</ul>";
}
return $output;
}
/**
* Returns an array of children leaf objects
* - called from _ezdown_build_links()
* - which is called from ezdown_view().
*/
function _ezdown_find_childern($leaf_id) {
global $user;
if (!isset($user)) {
$user_object = user_load(array('uid' => $uid));
}else {
$user_object = $user;
}
$leaves = array();
$result = db_query("SELECT * from {leaf} WHERE parentid = %d", $leaf_id);
if (db_num_rows($result) > 0) {
while($leaf = db_fetch_object($result)) {
$node = node_load(array('nid' => $leaf->nid), $revision = NULL, $reset = NULL);
if (node_access("view", $node, $user->uid)) {
array_push($leaves, $node);
}
}
}
usort($leaves, '_titlecmp');
return $leaves;
}
/**
* User-defined title comparison callback-function to be used with usort()
* called from _ezdown_find_childern()
* which is called from _ezdown_build_links()
* which is called from ezdown_view().
*/
function _titlecmp($a, $b) {
return strcmp($a->title, $b->title);
}
?>
:)
i have also made a zip file, but it looks like this forum is too basic to attach files
:(
hmm
updating was buggy (the pid got set incorrectly in the leaf table)
i changed the name of pid to parentid and all is fine
looks like 'pid' is used by drupal internally and is a poor choice for a field name
That's awesome
Thanks for your contribution ... It all looks great, I just want to do a quick install and run on both databases before I commit it. You actually can submit patches in the issue tracking system. (i.e. attach files) Do you have CVS access?
In the mean time, interested taking a look verison 2.0? It's starting to form up. I am using an actual tree data structure made from classes, arrays, and references. Not only that, it's going to have the ability to create a node tree based on the filesystem. I am currently working on removeing the upload module dependacy and have it hadle it's own file uploads. Hopefully I will also have time to put a caching and locking mechanism on the tree structure. I would be happy to set up some time to go over it with you
--Trevor
Postgres testing for 4.7
Hi,
I did some testing on postegres
and had to make a few changes
in ezdown.module i had to change one query to make
it compatible
/**
* Implementation of hook_update().
*
* As an existing node is being updated in the database, we need to do our own
* database updates.
*/
function ezdown_update($node) {
if ($node->nid && $node->parentid && $node->leaftype) {
db_query("UPDATE {leaf}
SET
parentid = '" .$node->parentid. "',
leaftype = '" .$node->leaftype. "',
attachment = NULL
WHERE
nid =" .$node->nid. ";");
}
}
and i had to change the install file a bit too
<?php
/**
* Implementation of hook_install().
*/
function ezdown_install() {
global $user;
$node = new StdClass();
$node->type = 'ezdown';
$node->uid = $user->uid;
$node->status = 1;
$node->promote = 0;
$node->sticky = 0;
$node->title = 'All Downloads';
node_save($node);
$nid = $node->nid;
switch ($GLOBALS['db_type']) {
case 'mysql':
case 'mysqli':
db_query("CREATE TABLE {leaf} (
nid integer NOT NULL,
parentid integer,
leaftype character(1),
attachment text,
PRIMARY KEY (nid)
) TYPE=MyISAM /*!40100 DEFAULT CHARACTER SET utf8 */;");
db_query("INSERT INTO {leaf}
(nid, parentid, leaftype)
VALUES (%d, %d, '%s')", $nid, -1, 'd');
break;
case 'pgsql':
db_query("CREATE TABLE {leaf} (
nid integer NOT NULL,
parentid integer,
leaftype character(1),
attachment text
);");
db_query("ALTER TABLE ONLY {leaf}
ADD CONSTRAINT leaf_pkey
PRIMARY KEY (nid);");
db_query("INSERT into {leaf}
(nid, parentid, leaftype, attachment)
VALUES (".$node->nid.", -1, 'd', '');");
break;
}
}
function ezdown_update_1() {
return _system_update_utf8(array('leaf'));
}
function ezdown_update_2() {
return db_query("ALTER TABLE `leaf` CHANGE `pid` `parentid` INT( 11 ) NULL DEFAULT NULL;");
}
?>
Hey, i posted the zip
Hey,
i posted the zip file(s) to the issue tracking system
http://drupal.org/node/71103
for 2.0 testing, etc
you can send me an email @http://drupal.org/user/62109/contact
I am super new at drupal,
so I hope I can help out in some way :)
some thoughts about document management:
- handling uploads and being able to choose a preexisting file from a dropdown would be very nice
- also providing custom icons for different file types, and making the tree collapsible would also rock
- providing extra info like file size right in the list view would be cool
- having the ability to check in and check out documents might be useful
- providing a license page/popup for some downloads might be nice
- the ability to search and view the search results as a document tree would be pretty cool (i.e. show all powerpoint files)
- (maybe) ezdownload should hide empty folders from users without the 'administer ezdown' permission
one caveat to handling uploads and downloads natively is the need to make sure
you can still have a document firewall where the documents live outside the webroot and are served up by php
New 2.0
Hi Guys -
It look like you've been hard at work on a new revision. Love the ideas of collapsible tree, etc. Is there any time frame for a new release? Just checking.
Thanks
Chris