Ir al contenido principal

Ralsina.Me — El sitio web de Roberto Alsina

Opening and Saving Files made easier.

I got tired of rewrit­ing the open/save log­ic on ev­ery ap­pli­ca­tion, so I turned it in­to a mod­ule.

It's your old tired open/save stuff,w ith a few twist­s. For starter­s, since the app I was do­ing it for (Not­ty) cre­ates large files, I added com­pres­sion (Sad­ly, I could­n't make it gzip-­com­pat­i­ble).

And since some files for that app could ben­e­fit from se­cre­cy, I added en­crip­tion, us­ing ro­­tor.

It al­so does atom­ic saves (that means the users can't open the file be­fore it's com­plete­ly saved).

How easy it was to do those things shows just how neat the Python Stan­dard Li­brary is.

I don't think the code is very nice, and there may even be se­ri­ous bugs in it. If you fig­ure out some prob­lem, just let me know!

While this mod­ule de­pends on PyQt, a PyKDE (or PyGTK or what­ev­er) ver­sion should be triv­ial. So should be a ver­sion for CLI app­s, I sup­pose.

Sim­ple scripts to change pass­words on en­crypt­ed files, or de­crypt them, or com­press or un­com­press them should be ob­vi­ous.

Maybe some­one want to con­trib­ute a script that does all the above (hint hint wink wink :-)

Here's the source


##~ ­Copy­right (c) 2004 Rober­to Alsi­na <ralsi­na@kde.org>
##~
##~ Per­mis­sion is here­by ­grant­ed, free of charge, ­to any per­son­ ob­tain­ing a
##~ ­copy­ of this ­soft­ware and as­so­ci­at­ed ­doc­u­men­ta­tion ­files (the
##~ "­Soft­ware"), ­to deal in the ­Soft­ware with­out re­stric­tion, in­clud­ing
##~ with­out lim­i­ta­tion the right­s ­to use, ­copy­, ­mod­i­fy, merge, pub­lish,
##~ dis­tribute, ­sub­li­cense, and/or sel­l ­copies of the ­Soft­ware, and ­to per­mit
##~ per­son­s ­to whom the ­Soft­ware is ­fur­nished ­to ­do ­so, ­sub­jec­t ­to the
##~ ­fol­low­ing ­con­di­tion­s:
##~
##~ The above ­copy­right no­tice and this per­mis­sion no­tice shal­l be in­clud­ed
##~ in al­l ­copies or ­sub­stan­tial ­por­tion­s of the ­Soft­ware.
##~
##~ THE ­SOFT­WARE IS PRO­VID­ED "AS IS", WITH­OUT WAR­RAN­TY OF ANY KIND, ­EX­PRESS
##~ OR IM­PLIED, IN­CLUD­ING BUT NOT LIM­IT­ED ­TO THE WAR­RANTIES OF
##~ MER­CHANTABIL­I­TY, ­FIT­NESS ­FOR A ­PAR­TIC­U­LAR PUR­POSE AND NON­IN­FRINGE­MEN­T.
##~ IN NO EVEN­T SHAL­L THE AU­THORS OR ­COPY­RIGHT HOLD­ER­S BE LI­ABLE ­FOR ANY
##~ ­CLAIM, ­DAM­AGES OR OTHER LI­A­BIL­I­TY, WHETHER IN AN AC­TION OF ­CON­TRAC­T,
##~ ­TORT OR OTHER­WISE, ARIS­ING FROM, OUT OF OR IN ­CON­NEC­TION WITH­ THE
##~ ­SOFT­WARE OR THE USE OR OTHER DEAL­INGS IN THE ­SOFT­WARE.


"""
This ­mod­ule ­con­tain­s ­func­tion­s im­ple­ment­ing a ­gener­ic open/save
in­ter­face ­for ap­pli­ca­tion­s. It ­sup­port­s reg­u­lar, ­com­pressed and/or
en­crypt­ed ­files, with­out hav­ing ­to ­make y­our ap­pli­ca­tion ­more ­com­plex.

As ­long as y­ou ­can pick­le y­our ­data in­to a string, this should be easy
to use and just ­work.

The ­com­pressed ­files are ­com­pressed us­ing python's ­zlib, the en­cryp­tion
is ­made us­ing python's ro­tor.

If y­ou ­don't ­do au­tosaves, and y­ou ­don't ­mind the user hav­ing ­to ­type
the ­pass­word on al­l save ­op­er­a­tions, y­ou ­can hap­pi­ly ig­nore the "key"
pa­ram­e­ter­s and re­turn ­val­ues, and ev­ery­thing should ­work any­way.

The save is atomic: ­First we save ­to a tem­po­ral ­file, then re­name it
in­to the de­sired ­name. This is ­so noone ­can open the ­file while it's not
com­plete­ly saved.

KNOWN IS­SUES:

* It will use lot­s of mem­o­ry ­for a large ­file, be­cause it ­keep­s it ­ful­ly
in­ mem­o­ry (­won't ­fix)

* A ­file en­crypt­ed with­ a nul­l ­pass­word is not en­crypt­ed (­won't ­fix)

* ­Com­pressed ­files are not ­gun­zip/gzip ­com­pat­i­ble. (­may ­fix)

* No way ­to ­choose the ­fil­ter­ing in the save ­di­alog (­may ­fix)

* ­Could be ­clean­er by re­mov­ing al­l ­di­alog ­code and/or ­mak­ing it op­tion­al
and/or adding ­more ­func­tion­s ­for it. (­won't ­fix)

"""


__au­thor__   =   'Rober­to Alsi­na <ralsi­na@kde.org>'
__li­cense__   =   'MIT/X11'


im­port   qt
im­port   zlib
im­port   trace­back
im­port   temp­file
im­port   os
im­port   shutil

def   en­crypt(da­ta,key=None):
        
"""

        Helper ­func­tion, en­crypt­s ­data us­ing key, with­ a 12-wheel ro­tor.

        Re­turn­s the en­crypt­ed ­da­ta. If key==None, it's not en­crypt­ed
        """

        
if   not   key:   #No key, no en­cryp­tion
                
re­turn   da­ta
        
im­port   ro­tor
        
rt=ro­tor.newro­tor(key,12)
        
re­turn   rt.en­crypt(da­ta)

def   de­crypt(da­ta,key=None):
        
"""

        Helper ­func­tion, de­crypt­s ­data us­ing key, with­ a 12-wheel ro­tor

        Re­turn­s the de­crypt­ed ­da­ta. Re­mem­ber that de­cryp­tion AL­WAYS re­turns
        ­some­thing. I ­sug­gest y­ou use ­some ­mag­ic ­cook­ie at the be­gin­ning
        ­to see if the ­data re­turned ­makes sense.

        If key==None, it's not de­crypt­ed

        """

        
if   not   key:   #No key, no en­cryp­tion
                
re­turn   da­ta
        
im­port   ro­tor
        
rt=ro­tor.newro­tor(key,12)
        
re­turn   rt.de­crypt(da­ta)


def   save­File(fname,da­ta,key=None):
        
"""

        ­data: a string ­con­tain­ing the ­data ­to ve saved.
        f­name: ­name of the ­file where y­ou wan­t ­to save.

        If the ­name end­s in .­com­p, it will be saved ­com­pressed us­ing ­zlib.
        If the ­name end­s in .ro­tor, it will be saved en­crypt­ed us­ing the key.
        If the ­name end­s in .­com­p.ro­tor, it will be ­com­pressed, then
        en­crypt­ed.

        Us­ing .ro­tor.­com­p ­makes no sense, be­cause en­crypt­ed ­files ­don't
        ­com­press at al­l, ­so it's not ­sup­port­ed.

        key is a string used ­for ro­tor en­cryp­tion
        If a key is re­quired but not pro­vid­ed, a ­di­alog will pop­up ask­ing
        ­for it.

        If there is an er­ror when ­com­press­ing, or writ­ing, or any­thing else,
        it will pop­up a er­ror mes­sage.

        The re­turn ­val­ue on ­suc­cess will be True,key with­ key be­ing the
        en­crip­tion key or None.

        On er­ror, it will re­turn ­False,key with­ key be­ing the en­crip­tion
        key or None.

        """

        
temp­file.tem­pdir=os.path.dirname(fname)
        
tname=temp­file.mk­temp()
        
if   os.ac­cess(fname,os.W_OK)   or   not   os.ac­cess(fname,os.F_OK):
                
ex­ten­sions=fname.split('.')
                
try:
                        
if   ex­ten­sions[-2]=='com­p'   or   ex­ten­sions[-1]=='com­p':   #A ­com­pressed ­file
                                
da­ta=zlib.com­press(da­ta,9)
                        
if   ex­ten­sions[-1]=='ro­tor':   #A ro­tor-crypt­ed ­file

                                
if   not   key:
                                        
(key,ok)=qt.QIn­put­Di­a­log.get­Text("Not­ty - ­Pass­word",
                                                                
"Please en­ter the ­pass­word",
                                                                
qt.QLi­neEd­it.Pass­word)
                                        
if   ok:
                                                
key=str(key)
                                
da­ta=en­crypt(da­ta,key)
                        
f=open(tname,'w­b')
                        
f.write(da­ta)
                        
f.flush()
                        
f.close()
                        
if   os.ac­cess(fname,os.F_OK):   #File ex­ists
                                
shutil.copy­mode(fname,tname)
                        
os.re­name(tname,fname)
                        
re­turn   (True,key)
                
ex­cept:
                        
trace­back.print­_exc()
                        
try:
                                
os.un­link(tname)
                        
ex­cept:
                                
# There re­al­ly is­n't ­much we ­can ­do :-)
                                
pass
        
QMes­sage­Box.crit­i­cal(None,'Er­ror - Not­ty',
                        
'Er­ror writ­ing ­to ­file "%s"'%fname,'Ok')
        
re­turn   (False,key)

def   open­File(fname,key=None):
        
"""

        f­name: ­name of the ­file y­ou wan­t ­to open. If no ­name is ­given,
        the ­s­tan­dard­ ­file ­di­alog will pop, ask­ing ­for one.

        If the ­name end­s in .­com­p, it will be un­com­pressed us­ing ­zlib.
        If the ­name end­s in .ro­tor, it will be de­crypt­ed us­ing the key.
        If the ­name end­s in .­com­p.ro­tor, it will be de­crypt­ed, then
        un­com­pressed.

        Us­ing .ro­tor.­com­p ­makes no sense, be­cause en­crypt­ed ­files ­don't
        ­com­press at al­l, ­so it's not ­sup­port­ed.

        key is a string used ­for ro­tor en­cryp­tion

        If a key is re­quired but not pro­vid­ed, a ­di­alog will pop­up ask­ing
        ­for it.

        De­crypt­ing will **n­ev­er** ­fail, ­so re­mem­ber that when y­ou pro­cess
        the ­code, ­may­be al­l y­ou get is ­garbage, ­so im­ple­men­t ­some er­ror
        check­ing ;-)

        If there is an er­ror when de­com­press­ing, or read­ing, or any­thing
        else, it will pop­up a er­ror mes­sage.

        The re­turn ­val­ue on ­suc­cess will be ­file­name,key with­ key be­ing
        the en­crip­tion key or None.

        On er­ror, it will re­turn '',key with­ key be­ing the en­crip­tion
        key or None.

        """

        
if   fname=='':   #Emp­ty ­file­name, ask ­for one
                
fname=str(qt.QFile­Di­a­log.getOpen­File­Name(os.getcwd()))
                
if   not   fname:
                        
re­turn   None
                
else:
                        
re­turn   open­File(fname)

        
ex­ten­sions=fname.split('.')

        
if   os.ac­cess(fname,os.F_OK)   and   os.ac­cess(fname,os.R_OK):
                
f=open(fname,'r­b')
                
da­ta=f.read()
                
pos=-1
                
if   ex­ten­sions[pos]=='ro­tor':
                        
if   not   key:
                                
(key,ok)=qt.QIn­put­Di­a­log.get­Text("Not­ty - ­Pass­word",
                                                
"Please en­ter the ­pass­word",
                                                
qt.QLi­neEd­it.Pass­word)
                                
key=str(key)
                        
if   not   key   or   not   ok:   #No key, no need ­to de­crypt
                                
pass
                        
else:
                                
da­ta=de­crypt(da­ta,key)
                        
pos=-2
                
if   ex­ten­sions[pos]=='com­p':   #Com­pressed ­file
                        
da­ta=zlib.de­com­press(da­ta)
                
re­turn   da­ta,key
        
elif   not   (os.ac­cess(fname,os.F_OK)):   #File ­does­n't ex­ist
                
try:
                        
f=open(fname,'w­b')   #But we ­can cre­ate it
                        
re­turn   '',key
                
ex­cept:   #And we ­can't cre­ate it
                        
pass

        
qt.QMes­sage­Box.crit­i­cal(None,'Er­ror - Not­ty',
                        
'Er­ror read­ing ­file "%s"'%fname,'Ok')
        
re­turn   False

def   saveAs­File(da­ta,key):
        
"""

        ­data is a string ­con­tain­ing the ­data ­to be saved.
        key is a string used ­for ro­tor en­cryp­tion

        It will pop­up a ­file ­di­alog ask­ing ­for a ­file­name ­to save, then will
        ­cal­l save­File with­ that ­file­name.

        If a key is re­quired but not pro­vid­ed, a ­di­alog will pop­up ask­ing
        ­for it.

        The re­turn ­val­ue on ­suc­cess will be True,­file­name,key with­ key be­ing
        the en­crip­tion key or None.

        On er­ror, it will re­turn ­False,­file­name,key with­ key be­ing the
        en­crip­tion key or None.

        """

        
fname=str(qt.QFile­Di­a­log.get­Save­File­Name(os.getcwd()))
        
if   not   fname:
                
re­turn   False,'',None
        
else:
                
sta­tus,key=save­File(fname,da­ta,key)
                
re­turn   sta­tus,key,fname
Roberto Alsina / 2006-04-04 16:28:

Comments for this story are here:

http://www.haloscan.com/com...


Contents © 2000-2023 Roberto Alsina