witten
/
luminotes
Archived
1
0
Fork 0

Factored out file upload methods from Notebooks to new Files controller.

Changed file link insertion code to reuse existing link creation code.
This commit is contained in:
Dan Helfman 2008-02-01 19:17:10 +00:00
parent 0cf2b5bda7
commit e56503903b
7 changed files with 273 additions and 244 deletions

224
controller/Files.py Normal file
View File

@ -0,0 +1,224 @@
import cgi
import cherrypy
from cherrypy.filters import basefilter
from Expose import expose
from Validate import validate
from Database import Valid_id
from Users import grab_user_id
from Expire import strongly_expire
from view.Upload_page import Upload_page
class Access_error( Exception ):
def __init__( self, message = None ):
if message is None:
message = u"Sorry, you don't have access to do that. Please make sure you're logged in as the correct user."
Exception.__init__( self, message )
self.__message = message
def to_dict( self ):
return dict(
error = self.__message
)
class Upload_error( Exception ):
def __init__( self, message = None ):
if message is None:
message = u"An error occurred when uploading the file."
Exception.__init__( self, message )
self.__message = message
def to_dict( self ):
return dict(
error = self.__message
)
class File_upload_filter( basefilter.BaseFilter ):
def before_request_body( self ):
if cherrypy.request.path != "/files/upload_file":
return
if cherrypy.request.method != "POST":
raise Upload_error()
# tell CherryPy not to parse the POST data itself for this URL
cherrypy.request.processRequestBody = False
class Files( object ):
_cpFilterList = [ File_upload_filter() ]
"""
Controller for dealing with uploaded files, corresponding to the "/files" URL.
"""
def __init__( self, database, users ):
"""
Create a new Files object.
@type database: controller.Database
@param database: database that files are stored in
@type users: controller.Users
@param users: controller for all users
@rtype: Files
@return: newly constructed Files
"""
self.__database = database
self.__users = users
@expose( view = Upload_page )
@validate(
notebook_id = Valid_id(),
note_id = Valid_id(),
)
def upload_page( self, notebook_id, note_id ):
"""
Provide the information necessary to display the file upload page.
@type notebook_id: unicode
@param notebook_id: id of the notebook that the upload will be to
@type note_id: unicode
@param note_id: id of the note that the upload will be to
@rtype: unicode
@return: rendered HTML page
"""
return dict(
notebook_id = notebook_id,
note_id = note_id,
)
@expose()
@strongly_expire
@grab_user_id
@validate(
user_id = Valid_id( none_okay = True ),
)
def upload_file( self, user_id ):
"""
Upload a file from the client for attachment to a particular note.
@type notebook_id: unicode
@param notebook_id: id of the notebook that the upload is to
@type note_id: unicode
@param note_id: id of the note that the upload is to
@raise Access_error: the current user doesn't have access to the given notebook or note
@rtype: unicode
@return: rendered HTML page
"""
cherrypy.server.max_request_body_size = 0 # remove file size limit of 100 MB
cherrypy.response.timeout = 3600 # increase upload timeout to one hour (default is 5 min)
cherrypy.server.socket_timeout = 60 # increase socket timeout to one minute (default is 10 sec)
# TODO: increase to 8k
CHUNK_SIZE = 1#8 * 1024 # 8 Kb
headers = {}
for key, val in cherrypy.request.headers.iteritems():
headers[ key.lower() ] = val
try:
file_size = int( headers.get( "content-length", 0 ) )
except ValueError:
raise Upload_error()
if file_size <= 0:
raise Upload_error()
parsed_form = cgi.FieldStorage( fp = cherrypy.request.rfile, headers = headers, environ = { "REQUEST_METHOD": "POST" }, keep_blank_values = 1)
upload = parsed_form[ u"file" ]
notebook_id = parsed_form.getvalue( u"notebook_id" )
note_id = parsed_form.getvalue( u"note_id" )
filename = upload.filename.strip()
if not self.__users.check_access( user_id, notebook_id ):
raise Access_error()
def process_upload():
"""
Process the file upload while streaming a progress meter as it uploads.
"""
progress_bytes = 0
fraction_reported = 0.0
progress_width_em = 20
tick_increment = 0.01
progress_bar = u'<img src="/static/images/tick.png" style="width: %sem; height: 1em;" id="progress_bar" />' % \
( progress_width_em * tick_increment )
yield \
u"""
<html>
<head>
<link href="/static/css/upload.css" type="text/css" rel="stylesheet" />
<script type="text/javascript" src="/static/js/MochiKit.js"></script>
<meta content="text/html; charset=UTF-8" http_equiv="content-type" />
</head>
<body>
"""
if not filename:
yield \
u"""
<div class="field_label">upload error: </div>
Please check that the filename is valid.
"""
return
base_filename = filename.split( u"/" )[ -1 ].split( u"\\" )[ -1 ]
yield \
u"""
<div class="field_label">uploading %s: </div>
<table><tr>
<td><div id="progress_border">
%s
</div></td>
<td></td>
<td><span id="status"></span></td>
</tr></table>
<script type="text/javascript">
function tick( fraction ) {
setElementDimensions(
"progress_bar",
{ "w": %s * fraction }, "em"
);
if ( fraction == 1.0 )
replaceChildNodes( "status", "100%%" );
else
replaceChildNodes( "status", Math.floor( fraction * 100.0 ) + "%%" );
}
</script>
""" % ( cgi.escape( base_filename ), progress_bar, progress_width_em )
import time
while True:
chunk = upload.file.read( CHUNK_SIZE )
if not chunk: break
progress_bytes += len( chunk )
fraction_done = float( progress_bytes ) / float( file_size )
if fraction_done > fraction_reported + tick_increment:
yield '<script type="text/javascript">tick(%s)</script>;' % fraction_reported
fraction_reported += tick_increment
time.sleep(0.025) # TODO: removeme
# TODO: write to the database
if fraction_reported == 0:
yield "An error occurred when uploading the file."
return
# the file finished uploading, so fill out the progress meter to 100%
if fraction_reported < 1.0:
yield '<script type="text/javascript">tick(1.0)</script>;'
yield \
u"""
</script>
</body>
</html>
"""
upload.file.close()
cherrypy.request.rfile.close()
return process_upload()

View File

@ -1,7 +1,5 @@
import re
import cgi
import cherrypy
from cherrypy.filters import basefilter
from datetime import datetime
from Expose import expose
from Validate import validate, Valid_string, Validation_error, Valid_bool
@ -17,7 +15,6 @@ from model.User_revision import User_revision
from view.Main_page import Main_page
from view.Json import Json
from view.Html_file import Html_file
from view.Upload_page import Upload_page
class Access_error( Exception ):
@ -34,36 +31,8 @@ class Access_error( Exception ):
)
class Upload_error( Exception ):
def __init__( self, message = None ):
if message is None:
message = u"An error occurred when uploading the file."
Exception.__init__( self, message )
self.__message = message
def to_dict( self ):
return dict(
error = self.__message
)
class File_upload_filter( basefilter.BaseFilter ):
def before_request_body( self ):
if cherrypy.request.path != "/notebooks/upload_file":
return
if cherrypy.request.method != "POST":
raise Upload_error()
# tell CherryPy not to parse the POST data itself for this URL
cherrypy.request.processRequestBody = False
class Notebooks( object ):
WHITESPACE_PATTERN = re.compile( u"\s+" )
_cpFilterList = [ File_upload_filter() ]
"""
Controller for dealing with notebooks and their notes, corresponding to the "/notebooks" URL.
"""
@ -1125,149 +1094,3 @@ class Notebooks( object ):
result[ "count" ] = count
return result
@expose( view = Upload_page )
@validate(
notebook_id = Valid_id(),
note_id = Valid_id(),
)
def upload_page( self, notebook_id, note_id ):
"""
Provide the information necessary to display the file upload page.
@type notebook_id: unicode
@param notebook_id: id of the notebook that the upload will be to
@type note_id: unicode
@param note_id: id of the note that the upload will be to
@rtype: unicode
@return: rendered HTML page
"""
return dict(
notebook_id = notebook_id,
note_id = note_id,
)
@expose()
@strongly_expire
@grab_user_id
@validate(
user_id = Valid_id( none_okay = True ),
)
def upload_file( self, user_id ):
"""
Upload a file from the client for attachment to a particular note.
@type notebook_id: unicode
@param notebook_id: id of the notebook that the upload is to
@type note_id: unicode
@param note_id: id of the note that the upload is to
@raise Access_error: the current user doesn't have access to the given notebook or note
@rtype: unicode
@return: rendered HTML page
"""
cherrypy.server.max_request_body_size = 0 # remove file size limit of 100 MB
cherrypy.response.timeout = 3600 # increase upload timeout to one hour (default is 5 min)
cherrypy.server.socket_timeout = 60 # increase socket timeout to one minute (default is 10 sec)
# TODO: increase to 8k
CHUNK_SIZE = 1#8 * 1024 # 8 Kb
headers = {}
for key, val in cherrypy.request.headers.iteritems():
headers[ key.lower() ] = val
try:
file_size = int( headers.get( "content-length", 0 ) )
except ValueError:
raise Upload_error()
if file_size <= 0:
raise Upload_error()
parsed_form = cgi.FieldStorage( fp = cherrypy.request.rfile, headers = headers, environ = { "REQUEST_METHOD": "POST" }, keep_blank_values = 1)
upload = parsed_form[ u"file" ]
notebook_id = parsed_form.getvalue( u"notebook_id" )
note_id = parsed_form.getvalue( u"note_id" )
filename = upload.filename.strip()
if not self.__users.check_access( user_id, notebook_id ):
raise Access_error()
def process_upload():
"""
Process the file upload while streaming a progress meter as it uploads.
"""
progress_bytes = 0
fraction_reported = 0.0
progress_width_em = 20
tick_increment = 0.01
progress_bar = u'<img src="/static/images/tick.png" style="width: %sem; height: 1em;" id="progress_bar" />' % \
( progress_width_em * tick_increment )
yield \
u"""
<html>
<head>
<link href="/static/css/upload.css" type="text/css" rel="stylesheet" />
<script type="text/javascript" src="/static/js/MochiKit.js"></script>
<meta content="text/html; charset=UTF-8" http_equiv="content-type" />
</head>
<body>
"""
if not filename:
yield \
u"""
<div class="field_label">upload error: </div>
Please check that the filename is valid.
"""
return
base_filename = filename.split( u"/" )[ -1 ].split( u"\\" )[ -1 ]
yield \
u"""
<div class="field_label">uploading %s: </div>
<div id="progress_border">
%s
</div>
<script type="text/javascript">
function tick( fraction ) {
setElementDimensions(
"progress_bar",
{ "w": %s * fraction }, "em"
);
}
</script>
""" % ( cgi.escape( base_filename ), progress_bar, progress_width_em )
import time
while True:
chunk = upload.file.read( CHUNK_SIZE )
if not chunk: break
progress_bytes += len( chunk )
fraction_done = float( progress_bytes ) / float( file_size )
if fraction_done > fraction_reported + tick_increment:
yield '<script type="text/javascript">tick(%s)</script>;' % fraction_reported
fraction_reported += tick_increment
time.sleep(0.025) # TODO: removeme
# TODO: write to the database
if fraction_reported == 0:
yield "An error occurred when uploading the file."
return
# the file finished uploading, so fill out the progress meter to 100%
if fraction_reported < 1.0:
yield '<script type="text/javascript">tick(1.0)</script>;'
yield \
u"""
</script>
</body>
</html>
"""
upload.file.close()
cherrypy.request.rfile.close()
return process_upload()

View File

@ -5,6 +5,7 @@ from Expire import strongly_expire
from Validate import validate, Valid_int, Valid_string
from Notebooks import Notebooks
from Users import Users, grab_user_id
from Files import Files
from Database import Valid_id
from model.Note import Note
from model.Notebook import Notebook
@ -43,6 +44,7 @@ class Root( object ):
settings[ u"global" ].get( u"luminotes.rate_plans", [] ),
)
self.__notebooks = Notebooks( database, self.__users )
self.__files = Files( database, self.__users )
@expose( Main_page )
@grab_user_id
@ -353,3 +355,4 @@ class Root( object ):
database = property( lambda self: self.__database )
notebooks = property( lambda self: self.__notebooks )
users = property( lambda self: self.__users )
files = property( lambda self: self.__files )

View File

@ -39,6 +39,10 @@ div {
height: 1em;
}
td {
vertical-align: top;
}
#tick_preload {
height: 0;
overflow: hidden;

View File

@ -416,7 +416,7 @@ Editor.prototype.empty = function () {
return ( scrapeText( this.document.body ).length == 0 );
}
Editor.prototype.start_link = function () {
Editor.prototype.insert_link = function ( url ) {
// get the current selection, which is the link title
if ( this.iframe.contentWindow && this.iframe.contentWindow.getSelection ) { // browsers such as Firefox
var selection = this.iframe.contentWindow.getSelection();
@ -428,7 +428,7 @@ Editor.prototype.start_link = function () {
var placeholder = withDocument( this.document, function () { return getElement( "placeholder_title" ); } );
selection.selectAllChildren( placeholder );
this.exec_command( "createLink", "/notebooks/" + this.notebook_id + "?note_id=new" );
this.exec_command( "createLink", url );
selection.collapseToEnd();
// hack to prevent Firefox from erasing spaces before links that happen to be at the end of list items
@ -443,7 +443,7 @@ Editor.prototype.start_link = function () {
// otherwise, just create a link with the selected text as the link title
} else {
this.link_started = null;
this.exec_command( "createLink", "/notebooks/" + this.notebook_id + "?note_id=new" );
this.exec_command( "createLink", url );
return this.find_link_at_cursor();
}
} else if ( this.document.selection ) { // browsers such as IE
@ -455,16 +455,24 @@ Editor.prototype.start_link = function () {
range.text = " ";
range.moveStart( "character", -1 );
range.select();
this.exec_command( "createLink", "/notebooks/" + this.notebook_id + "?note_id=new" );
this.exec_command( "createLink", url );
this.link_started = this.find_link_at_cursor();
} else {
this.link_started = null;
this.exec_command( "createLink", "/notebooks/" + this.notebook_id + "?note_id=new" );
this.exec_command( "createLink", url );
return this.find_link_at_cursor();
}
}
}
Editor.prototype.start_link = function () {
return this.insert_link( "/notebooks/" + this.notebook_id + "?note_id=new" );
}
Editor.prototype.start_file_link = function () {
return this.insert_link( "/files/new" );
}
Editor.prototype.end_link = function () {
this.link_started = null;
var link = this.find_link_at_cursor();
@ -492,46 +500,6 @@ Editor.prototype.end_link = function () {
return link;
}
Editor.prototype.insert_file_link = function ( filename, file_id ) {
// get the current selection, which is the link title
if ( this.iframe.contentWindow && this.iframe.contentWindow.getSelection ) { // browsers such as Firefox
var selection = this.iframe.contentWindow.getSelection();
// if no text is selected, then insert a link with the filename as the link title
if ( selection.toString().length == 0 ) {
this.insert_html( '<span id="placeholder_title">' + filename + '</span>' );
var placeholder = withDocument( this.document, function () { return getElement( "placeholder_title" ); } );
selection.selectAllChildren( placeholder );
this.exec_command( "createLink", "/files/" + file_id );
selection.collapseToEnd();
// replace the placeholder title span with just the filename, yielding an unselected link
var link = placeholder.parentNode;
link.innerHTML = filename;
link.target = "_new";
// otherwise, just create a link with the selected text as the link title
} else {
this.exec_command( "createLink", "/files/" + file_id );
var link = this.find_link_at_cursor();
link.target = "_new";
}
} else if ( this.document.selection ) { // browsers such as IE
var range = this.document.selection.createRange();
// if no text is selected, then insert a link with the filename as the link title
if ( range.text.length == 0 ) {
range.text = filename;
range.moveStart( "character", -1 * filename.length );
range.select();
}
this.exec_command( "createLink", "/files/" + file_id );
var link = this.find_link_at_cursor();
link.target = "_new";
}
}
Editor.prototype.find_link_at_cursor = function () {
if ( this.iframe.contentWindow && this.iframe.contentWindow.getSelection ) { // browsers such as Firefox
var selection = this.iframe.contentWindow.getSelection();
@ -582,18 +550,6 @@ Editor.prototype.find_link_at_cursor = function () {
return null;
}
Editor.prototype.node_at_cursor = function () {
if ( this.iframe.contentWindow && this.iframe.contentWindow.getSelection ) { // browsers such as Firefox
var selection = this.iframe.contentWindow.getSelection();
return selection.anchorNode;
} else if ( this.document.selection ) { // browsers such as IE
var range = this.document.selection.createRange();
return range.parentElement();
}
return null;
}
Editor.prototype.focus = function () {
if ( /Opera/.test( navigator.userAgent ) )
this.iframe.focus();

View File

@ -932,7 +932,21 @@ Wiki.prototype.update_toolbar = function() {
this.update_button( "title", "h3", node_names );
this.update_button( "insertUnorderedList", "ul", node_names );
this.update_button( "insertOrderedList", "ol", node_names );
this.update_button( "createLink", "a", node_names );
var link = this.focused_editor.find_link_at_cursor();
if ( link ) {
// determine whether the link is a note link or a file link
if ( link.target || !/\/files\//.test( link.href ) ) {
this.down_image_button( "createLink" );
this.up_image_button( "attachFile" );
} else {
this.up_image_button( "createLink" );
this.down_image_button( "attachFile" );
}
} else {
this.up_image_button( "createLink" );
this.up_image_button( "attachFile" );
}
}
Wiki.prototype.toggle_link_button = function ( event ) {
@ -975,7 +989,7 @@ Wiki.prototype.toggle_attach_button = function ( event ) {
this.clear_messages();
this.clear_pulldowns();
new Upload_pulldown( this, this.notebook_id, this.invoker, this.focused_editor, this.focused_editor.node_at_cursor() );
new Upload_pulldown( this, this.notebook_id, this.invoker, this.focused_editor );
}
event.stop();
@ -2210,16 +2224,17 @@ Link_pulldown.prototype.shutdown = function () {
this.link.pulldown = null;
}
function Upload_pulldown( wiki, notebook_id, invoker, editor, anchor ) {
this.anchor = anchor;
function Upload_pulldown( wiki, notebook_id, invoker, editor ) {
editor.start_file_link();
this.link = editor.find_link_at_cursor();
Pulldown.call( this, wiki, notebook_id, "upload_" + editor.id, anchor, editor.iframe );
Pulldown.call( this, wiki, notebook_id, "upload_" + editor.id, this.link, editor.iframe );
wiki.down_image_button( "attachFile" );
this.invoker = invoker;
this.editor = editor;
this.iframe = createDOM( "iframe", {
"src": "/notebooks/upload_page?notebook_id=" + notebook_id + "&note_id=" + editor.id,
"src": "/files/upload_page?notebook_id=" + notebook_id + "&note_id=" + editor.id,
"frameBorder": "0",
"scrolling": "no",
"id": "upload_frame",
@ -2237,10 +2252,11 @@ Upload_pulldown.prototype.constructor = Upload_pulldown;
Upload_pulldown.prototype.init_frame = function () {
var self = this;
var doc = this.iframe.contentDocument || this.iframe.contentWindow.document;
withDocument( this.iframe.contentDocument, function () {
withDocument( doc, function () {
connect( "upload_button", "onclick", function ( event ) {
withDocument( self.iframe.contentDocument, function () {
withDocument( doc, function () {
self.upload_started( getElement( "file" ).value );
} );
} );
@ -2254,7 +2270,10 @@ Upload_pulldown.prototype.upload_started = function ( filename ) {
pieces = filename.split( "\\" );
filename = pieces[ pieces.length - 1 ];
this.editor.insert_file_link( filename );
// the current title is blank, replace the title with the upload's filename
if ( link_title( this.link ) == "" )
replaceChildNodes( this.link, this.editor.document.createTextNode( filename ) );
// TODO: set the link's href to the file
}
Upload_pulldown.prototype.shutdown = function () {

View File

@ -16,7 +16,7 @@ class Upload_page( Html ):
Input( type = u"submit", id = u"upload_button", class_ = u"button", value = u"upload" ),
Input( type = u"hidden", id = u"notebook_id", name = u"notebook_id", value = notebook_id ),
Input( type = u"hidden", id = u"note_id", name = u"note_id", value = note_id ),
action = u"/notebooks/upload_file",
action = u"/files/upload_file",
method = u"post",
enctype = u"multipart/form-data",
),