root/trunk/pylucid_project/PyLucid/plugins_internal/filemanager/filemanager.py

Revision 1825, 18.7 KB (checked in by JensDiemer, 13 months ago)

Work-in-progress: move some code from filemanager plugin into a shared destination. It's usefull for "filesystem based plugins" such as the gallery. Change the settings for the filemanager base path. Every fs-plugin should use this path information.

  • Property svn:eol-style set to LF
  • Property svn:keywords set to Author Rev LastChangedDate
Line 
1# -*- coding: utf-8 -*-
2
3"""
4    PyLucid media file manager
5    ~~~~~~~~~~~~~~~~~~~~~~~~~~
6
7    We have two kinds of forms: POST actions and GET actions
8    POST actions are:
9        - filemanager.action_mkdir()
10        - filemanager.action_fileupload()
11        - filemanager.action_rmdir()
12        - filemanager.action_deletefile()
13    GET actions are:
14        - filemanager.edit() (Edit a text file)
15
16    POST action note:
17        The POST actions used a hidden field named "action" for easy differ
18        the the current action in the POST data. See below the constants.
19    GET action notes:
20        - The GET forms should not insert a field named "action"!
21        - After a finished GET method, it should display the filelist().
22
23
24    restrictions:
25        - Only tested under linux!
26
27    TODO:
28        - should use posixpath for every URL stuff.
29        - deny editing of binary files (how? ext whitelist or using file?)
30        - insert the basepath selection into the filelist view.
31        - find a way to reduce the redundance.
32        - Write a unitest for the plugin and verify the "bad-char-things" in
33            path/post variables.
34        - Fixe some unicode problems:
35            e.g.: upload a file with a non ascii content
36            use sys.getfilesystemencoding() ?
37        - Check the Plugin under windows - very low priority :)
38
39    Last commit info:
40    ~~~~~~~~~
41    $LastChangedDate$
42    $Rev$
43    $Author$
44
45    :copyleft: 2007-2008 by the PyLucid team, see AUTHORS for more details.
46    :license: GNU GPL v2 or above, see LICENSE for more details
47"""
48
49__version__= "$Rev: $"
50
51import os, cgi, sys, stat, dircache, time
52from datetime import datetime
53
54from django import forms
55from django.conf import settings
56from django.http import HttpResponse
57from django.forms import ValidationError
58from django.utils.translation import ugettext as _
59
60from PyLucid.system.BaseFilesystemPlugin import FilesystemPlugin
61from PyLucid.forms.filesystem import FilenameField, DirnameField
62
63#______________________________________________________________________________
64# We use more than one html form in a filelist page. So we need some unique
65# action values for a easier distinguish the POST data.
66ACTION_RMDIR = "0"
67ACTION_MKDIR = "1"
68ACTION_FILEUPLOAD = "2"
69ACTION_DELETEFILE = "3"
70
71#______________________________________________________________________________
72
73def make_dirlist(path, result=[]):
74    """
75    Helper function for building a directory link line.
76    used in filemanager.make_dir_links()
77
78    >>> make_dirlist("/data/one/two")
79    [('data/one/two', 'two'), ('data/one', 'one'), ('data', 'data')]
80    """
81    path = path.strip("/")
82
83    head, tail = os.path.split(path)
84    result.append((path, tail))
85
86    if head:
87        # go recusive deeper
88        return make_dirlist(head, result)
89    else:
90        result.reverse()
91        return result
92
93#______________________________________________________________________________
94
95class EditFileForm(forms.Form):
96    """ Edit a text file form """
97    filename = FilenameField(
98        help_text=_(
99            u"Change the filename,"
100            " if you want to save the content into a new file."
101        )
102    )
103    content = forms.CharField(
104        widget=forms.Textarea(attrs = {'cols': '80', 'rows': '25'})
105    )
106
107
108#______________________________________________________________________________
109
110class ActionField(forms.CharField):
111    """
112    A spezial HiddenInput field for the action string.
113    The action string is set in the forms.Form class and must be the same in
114    the POST data, otherwise the form is not valid.
115    """
116    def __init__(self, action):
117        self.action = action
118        max_length = min_length = len(action)
119        super(ActionField, self).__init__(
120            max_length=max_length, min_length=min_length,
121            required=True, initial=action,
122            widget=forms.HiddenInput,
123        )
124
125    def clean(self, value):
126        super(ActionField, self).clean(value)
127        if value != self.action:
128            raise ValidationError(_(u"Wrong action!"))
129        return value
130
131#______________________________________________________________________________
132
133class CreateDirForm(forms.Form):
134    """
135    Form for creating a new directory
136    ToDo: add a choice field for create a file or a directory action
137    """
138    action = ActionField(ACTION_MKDIR)
139    dirname = DirnameField(
140        help_text="Create a new directory into the current directory."
141    )
142
143
144class UploadFileForm(forms.Form):
145    """ Form to upload a new file """
146    action = ActionField(ACTION_FILEUPLOAD)
147    ufile = forms.FileField(
148        label="filename",
149        help_text="Upload a new file into the current directory."
150    )
151
152class RmDirForm(forms.Form):
153    """
154    Delete a directory.
155    """
156    action = ActionField(ACTION_RMDIR)
157    item_name = DirnameField(
158        help_text="Create a new directory into the current directory."
159    )
160
161class DeleteFileForm(forms.Form):
162    """
163    Delete one file.
164    """
165    action = ActionField(ACTION_DELETEFILE)
166    item_name = FilenameField()
167
168
169
170
171#______________________________________________________________________________
172
173class filemanager(FilesystemPlugin):
174    """
175    The PyLucid plugin class.
176    """
177    def get_filelist(self):
178        """
179        Returns all items in the given directory.
180        -rel_dir is relative to ABS_PATH
181        -the listing is sorted and the first items are the directories.
182        """
183        files = []
184
185        if self.path["rel_path"] == "":
186            # current dir is the media root
187            dirs=[]
188        else:
189            # Add the ".." dir item
190            updir = os.path.split(self.path["rel_path"])[0]
191            dirs = [{
192                "name": "..",
193                "link": self.URLs.methodLink(
194                    method_name="filelist", args=(self.path["base_key"], updir)
195                ),
196                "is_dir": True,
197                "deletable": False,
198                "dont_display_size": True,
199            }]
200
201        link_prefix = self.URLs.methodLink(
202            method_name="filelist", args=self.path["url_path"]
203        )
204
205        for item in sorted(os.listdir(self.path["abs_path"])):
206            if item.startswith("."):
207                # skip hidden files or directories
208                continue
209
210            abs_item_path = os.path.join(self.path["abs_path"], item)
211            statinfo = os.stat(abs_item_path)
212
213            if stat.S_ISDIR(statinfo.st_mode):
214                # Is a directory
215                is_dir = True
216                link = os.path.join(link_prefix, item) + "/"
217                size = len(dircache.listdir(
218                    os.path.join(self.path["abs_path"], item)
219                ))
220            else:
221                is_dir = False
222                link = self.path.get_abs_link(item)
223                size = statinfo.st_size
224
225            mtime = statinfo.st_mtime
226            localtime = time.gmtime(mtime)
227            localdatetime = datetime(*localtime[:6])
228
229            item_dict={
230                "name": item,
231                "link": link,
232                "is_dir": is_dir,
233                "title": abs_item_path,
234                "time": localdatetime,
235                "size": size,
236                "mode": statinfo.st_mode,
237                "uid": statinfo.st_uid,
238                "gid": statinfo.st_gid,
239                "deletable": True,
240            }
241            if is_dir:
242                dirs.append(item_dict)
243            else:
244                files.append(item_dict)
245
246        # return the merged list of direcories and files together
247        dir_list = dirs + files
248        return dir_list
249
250
251    def make_dir_links(self):
252        """
253        Build the context for the path link line.
254        Use the function make_dirlist().
255        """
256        # start with the first base_path entry:
257        dir_links = [{
258            "name": self.path["base_fs_path"], # use only the short relative path
259            "title": self.path["abs_path"],
260            "link": self.URLs.methodLink(
261                method_name="filelist", args=self.path["base_key"]
262            ),
263        }]
264        if self.path["rel_path"] != "":
265            # Not in the root
266            dirlist = make_dirlist(self.path["rel_path"], [])
267            for path, name in dirlist:
268                # append every dir "steps"
269                dir_links.append({
270                    "name": name,
271                    "title": os.path.join(self.path["base_fs_path"], path),
272                    "link": self.URLs.methodLink(
273                        method_name="filelist",
274                        args=(self.path["base_key"], path)
275                    ),
276                })
277
278        return dir_links
279
280    #--------------------------------------------------------------------------
281    # html GET actions:
282
283    def edit(self, path_info):
284        """
285        Edit a text file.
286        """
287        self.path.new_filename_path(path_info, must_exist=True)
288        #self.path.debug()
289
290        try:
291            f = file(self.path["abs_file_path"], "r")
292            content = f.read()
293            f.close()
294            content = content.decode(settings.FILE_CHARSET)
295        except Exception, e:
296            self.page_msg.red("Error, reading file:", e)
297            return
298
299        if self.request.method != 'POST':
300            form = EditFileForm({
301                "content": content,
302                "filename": self.path["filename"],
303            })
304        else: # POST
305            #self.page_msg(self.request.POST)
306            form = EditFileForm(self.request.POST)
307            if form.is_valid():
308                filename = form.cleaned_data["filename"]
309                content = form.cleaned_data["content"]
310                abs_file_path = os.path.join(self.path["abs_path"], filename)
311                try:
312                    content = content.encode(settings.FILE_CHARSET)
313                    f = file(abs_file_path, "w")
314                    f.write(content)
315                    f.close()
316                except Exception, e:
317                    self.page_msg.red("Error, writing file:", e)
318                else:
319                    self.page_msg.green(
320                        "New content saved into '%s'." % filename
321                    )
322                    # Display the filelist
323                    return self.filelist(self.path["url_path"])
324
325        # Don't include the filename in methodLink-args, it always append a
326        # slash!
327        form_link = self.URLs.methodLink(
328            method_name="edit", args=self.path["url_path"]
329        )
330        form_link += self.path["filename"]
331
332        file_path = self.path.get_abs_link()
333
334        # Change the global page title:
335        self.context["PAGE"].title = _("Edit file - %s" % file_path)
336
337        context = {
338            "form_link": form_link,
339            "url_abort": self.URLs.methodLink(
340                method_name="filelist", args=self.path["url_path"]
341            ),
342            "file_path": file_path,
343            "filename": self.path["filename"],
344            "form": form,
345            "charset": settings.FILE_CHARSET,
346        }
347        self._render_template("edit_file", context)#, debug=True)
348
349    #--------------------------------------------------------------------------
350    # html POST actions:
351
352    def action_mkdir(self, dirname):
353        """
354        create a new directory
355        """
356        abs_new_path = os.path.join(self.path["abs_path"], dirname)
357        try:
358            os.mkdir(abs_new_path)
359        except Exception, e:
360            self.page_msg.red("Can't create '%s': %s" % (dirname, e))
361        else:
362            self.page_msg.green("'%s' creaded successfull." % dirname)
363
364
365    def action_fileupload(self, ufile):
366        """
367        save a uploaded file.
368        """
369        filename = ufile.name
370        abs_fs_path = os.path.join(self.path["abs_path"], filename)
371        try:
372            f = file(abs_fs_path,'wb') # if it exists, overwrite
373
374            for chunk in ufile.chunks():
375                f.write(chunk)
376
377            f.close()
378        except Exception, e:
379            self.page_msg.red("Can't write file: '%s'" % e)
380            return
381
382        statinfo = os.stat(abs_fs_path)
383        real_filesize = statinfo.st_size
384
385        if real_filesize == ufile.size:
386            self.page_msg.green(
387                "File '%s' written successfull. (%s Bytes)" % (
388                    filename, real_filesize
389                )
390            )
391        else: # Should never appear
392            self.page_msg.red(
393                "Error writing file '%s'."
394                " Filesize is different:"
395                " Should be %s Bytes, but is %s Bytes" % (
396                    ufile.file_size, real_filesize
397                )
398            )
399
400
401    def action_rmdir(self, dirname):
402        """ delete a directory """
403        abs_fs_path = os.path.join(self.path["abs_path"], dirname)
404        try:
405            os.rmdir(abs_fs_path)
406        except Exception, e:
407            self.page_msg.red("Can't delete '%s': %s" % (dirname, e))
408        else:
409            self.page_msg.green("'%s' deleted successfull." % dirname)
410
411
412    def action_deletefile(self, filename):
413        """ delete a file """
414        abs_fs_path = os.path.join(self.path["abs_path"], filename)
415        try:
416            os.remove(abs_fs_path)
417        except Exception, e:
418            self.page_msg.red("Can't delete '%s': %s" % (filename, e))
419        else:
420            self.page_msg.green("File '%s' deleted successfull." % filename)
421
422    #--------------------------------------------------------------------------
423
424    def userinfo(self, old_path=""):
425        """
426        Display some user information related to the filemanager functionality.
427        """
428        # Change the global page title:
429        self.context["PAGE"].title = _(
430            "Filemanager - Display some user information"
431        )
432
433        import pwd, grp
434
435        uid = os.getuid()
436        gid = os.getgid()
437
438        pwd_info = pwd.getpwuid(os.getuid())
439        grp_info = grp.getgrgid(os.getgid())
440
441        context = {
442            "filelist_link": self.URLs.methodLink(
443                method_name="filelist", args=old_path
444            ),
445            "uid": uid,
446            "gid": gid,
447            "pwd_info": pwd_info,
448            "grp_info": grp_info,
449        }
450#        self.page_msg(context)
451        self._render_template("userinfo", context)#, debug=True)
452
453    def select_basepath(self):
454        """
455        change the basepath, after send the form, we display the filelist
456        """
457        # Change the global page title:
458        self.context["PAGE"].title = _("Filemanager - Change the basepath")
459       
460        context = {}
461
462        path_key = self.basepath_form(context)
463        if not path_key:
464            self._render_template("select_basepath", context)#, debug=True)
465        else:
466            # POST with valide form data -> display the filelist
467            self.filelist(path_key + u"/")           
468
469    #--------------------------------------------------------------------------
470
471    def filelist(self, path_info=None):
472        """
473        List dir and file. Some actions.
474        rest: path to dir or file
475        """       
476        if not path_info:
477            self.select_basepath()
478            return
479
480        # analyse and store the given GET path infomation
481        self.path.new_dir_path(path_info, must_exist=True)
482        #self.path.debug()
483
484        # Change the global page title:
485        path = os.path.join(self.path["base_fs_path"], self.path["rel_path"])
486        self.context["PAGE"].title = _("File list - %s" % path)
487
488        # We init all forms before we check the POST, because we only need
489        # error information for the current action. Only the form for the
490        # current action should be inited with self.request.POST!
491        ufile_form = UploadFileForm()
492        mkdir_form = CreateDirForm()
493
494        if self.request.method == 'POST' and "action" in self.request.POST:
495            #self.page_msg(self.request.POST)
496            action = self.request.POST["action"]
497
498            #------------------------------------------------------------------
499            # action in the current directory:
500
501            if action == ACTION_MKDIR:
502                # Create a new directory
503                mkdir_form = CreateDirForm(self.request.POST)
504                if mkdir_form.is_valid():
505                    dirname = mkdir_form.cleaned_data["dirname"]
506                    self.action_mkdir(dirname)
507
508            elif action == ACTION_FILEUPLOAD:
509                # save a uploaded file
510                ufile_form = UploadFileForm(
511                    self.request.POST, self.request.FILES
512                )
513                if ufile_form.is_valid():
514                    ufile = ufile_form.cleaned_data["ufile"]
515                    self.action_fileupload(ufile)
516
517            #------------------------------------------------------------------
518            # action for one file/directory:
519
520            elif action == ACTION_RMDIR:
521                # delete a directory
522                form = RmDirForm(self.request.POST)
523                if form.is_valid():
524                    dirname = form.cleaned_data["item_name"]
525                    self.action_rmdir(dirname)
526
527            elif action == ACTION_DELETEFILE:
528                # delete a file
529                form = DeleteFileForm(self.request.POST)
530                if form.is_valid():
531                    filename = form.cleaned_data["item_name"]
532                    self.action_deletefile(filename)
533
534        # build the directory+file list:
535        dir_list = self.get_filelist()
536
537        # Build the path link line:
538        dir_links = self.make_dir_links()
539
540        # if the current path is writeable?
541        writeable = os.access(self.path["abs_path"], os.W_OK)
542
543        context = {
544#            "path": self.path,
545            "post_url": self.URLs.methodLink(
546                method_name="filelist", args=self.path["url_path"]
547            ),
548
549            "mkdir_form": mkdir_form,
550            "ufile_form": ufile_form,
551
552            "ACTION_RMDIR": ACTION_RMDIR,
553            "ACTION_DELETEFILE": ACTION_DELETEFILE,
554
555            "dir_links": dir_links,
556            "writeable": writeable,
557            "dir_list": dir_list,
558
559            "userinfo_link": self.URLs.methodLink(
560                method_name="userinfo", args=self.path["url_path"]
561            ),
562            "edit_link": self.URLs.methodLink(
563                method_name="edit", args=self.path["url_path"]
564            ),
565            "change_basepath_link": self.URLs.methodLink(
566                method_name="select_basepath"
567            ),
568        }
569        #self.page_msg(context)
570        self._render_template("filelist", context)#, debug=True)
571
572# -----------------------------------------------------------------------------
573
574class WrongDirectory(Exception):
575    def __init__(self, value):
576        self.value = value
577    def __str__(self):
578        return repr(self.value)
579
580class BadFilename(Exception):
581    """ A not allowed character contain a filename """
582    pass
583
584class BadPath(Exception):
585    """ A not allowed character contain a path """
586    pass
Note: See TracBrowser for help on using the browser.