Annotation of OSAS/OSA_system/OSAS_archiver.py, revision 1.7

1.1       dwinter     1: # Methoden und Klassen fuer den MPIWG Archiver
                      2: 
                      3: from OFS.Image import Image
                      4: from OFS.Folder import Folder
                      5: from OFS.SimpleItem import SimpleItem
                      6: from Products.PageTemplates.PageTemplateFile import PageTemplateFile
                      7: from Products.PageTemplates.PageTemplate import PageTemplate
                      8: from AccessControl import ClassSecurityInfo
                      9: from Globals import InitializeClass
                     10: from Globals import Persistent
                     11: from Acquisition import Implicit
                     12: from OSAS_show import *
1.3       dwinter    13: from OSAS_helpers import *
1.5       dwinter    14: from types import *
1.1       dwinter    15: 
                     16: import os.path
                     17: import os
                     18: import OSAS_ids
                     19: import archive #Baustelle
1.3       dwinter    20: import time
                     21: 
                     22: class OSAS_archiveInbox(SimpleItem,Persistent,Implicit):
                     23:     """Inbox"""
                     24: 
                     25:     meta_type="OSAS_archiveInbox"
                     26:     pathes=[]
                     27:     
                     28:     def __init__(self,id,title):
                     29:         """init"""
                     30:         self.id=id
                     31:         self.title=title
                     32:         self.pathes=[]
                     33: 
                     34:     def addPath(self,path):
                     35:         today=time.localtime()
                     36:         self.pathes.append([path,today])
                     37: 
                     38:     def index_html(self):
                     39:         """main"""
                     40:         pt=PageTemplateFile('Products/OSA_system/OSAS_archiveInboxIndex.zpt').__of__(self)
                     41:         return pt()
                     42:     
                     43: def manage_AddOSAS_archiveInboxForm(self):
                     44:     """interface for adding the OSAS_root"""
                     45:     pt=PageTemplateFile('Products/OSA_system/AddOSAS_archiveInbox.zpt').__of__(self)
                     46:     return pt()
                     47: 
                     48: 
                     49: def manage_AddOSAS_archiveInbox(self,id,title="",RESPONSE=None):
                     50:     """add the OSAS_root"""
                     51:     if title=="":
                     52:         title=id
                     53:         
                     54:     newObj=OSAS_archiveInbox(id, title)
                     55:     self._setObject(id,newObj)
                     56:     if RESPONSE is not None:
                     57:         RESPONSE.redirect('manage_main')
                     58:     
                     59: 
1.1       dwinter    60: 
                     61: class OSAS_metadataOrganizer(SimpleItem,Persistent,Implicit):
                     62:     """Eingabe von Metadaten"""
                     63: 
                     64:     meta_type="OSAS_metadataOrganizer"
1.3       dwinter    65:     mediaTypes=["image","video","text","audio","data"]
                     66:     acquisitionTypes=["Image-Acquisition"]
                     67:     mediaToAcquisition={"image":"Image-Acquisition"}
                     68:     metaDataSets={'Image-Acquisition': [('device','opt'),('image-type','opt'),('production-comment','opt')]}
                     69:     imgData={'image':[('dpi','req')]}
                     70:     
                     71:     bibDataSets={'Book':[('author','opt'),('year','opt'),('title','opt'),('series editor','opt'),('series title','opt'),('series volume','opt'),('number of pages','opt'),('city','opt'),('publisher','opt'),('edition','opt'),('number of volumes','opt'),('translator','opt'),('ISBN ISSN','opt')],
                     72:                   'Journal Article':[('author','opt'),('year','opt'),('title','opt'),('journal','opt'),('volume','opt'),('issue','opt'),('pages','opt'),('alternate journal','opt'),('call number','opt')],
                     73:                   'Manuscript':[('author','opt'),('year','opt'),('title','opt'),('location','opt'),('signature','opt'),('pages','opt'),('editorial remarks','opt'),('description','opt'),('keywords','opt')]}
                     74: 
                     75: 
                     76:     referenceTypes=['Book','Journal Article','Manuscript']
1.1       dwinter    77: 
                     78:     def __init__(self,id,title):
                     79:         """init"""
                     80:         self.id=id
                     81:         self.title=title
                     82:         #self.acquisitionData=['provider_name','provider_address','provider_contact','provider_url','date','description']
                     83: 
1.6       dwinter    84: 
                     85:     def getName(self):
                     86:         """gives name from request session path"""
                     87:         path=self.REQUEST.SESSION['path']
                     88:         splitted=path.split("/")
                     89:         return splitted[len(splitted)-1]
                     90:         
                     91:     def addRessourceMeta(self,path=None,RESPONSE=None):
                     92:         """Metadaten fuer Ressource"""
                     93:         if not path:
                     94:             path=self.REQUEST.SESSION['path']
                     95: 
                     96:         else:
                     97:             self.REQUEST.SESSION['path']=path
                     98: 
                     99:         pt=PageTemplateFile('Products/OSA_system/inputRessourceData.zpt').__of__(self)
                    100:         return pt()
                    101: 
                    102:         
                    103:     def writeRessourceMetadata(self,name,date,description,creator,RESPONSE=None):
                    104:         """schreibe Resourcedata in index.meta"""
                    105: 
                    106:         path=self.REQUEST.SESSION['path']
                    107: 
                    108:         subnodes={}
                    109:         subnodes['name']=name
                    110:         subnodes['archive-creation-date']=date
                    111:         subnodes['creator']=creator       
                    112:         subnodes['description']=description
                    113:         
                    114:         changeNodesInIndexMeta(path,"",subnodes)
                    115:         self.inbox.addPath(self.REQUEST.SESSION['path'])
                    116:         RESPONSE.redirect(self.REQUEST['URL2'])
                    117:         
                    118:         
1.1       dwinter   119:     def addAcquisition(self,path):
                    120:         """Hinzufügen von Acquisition Daten"""
                    121:         self.REQUEST.SESSION['path']=path
                    122:         pt=PageTemplateFile('Products/OSA_system/inputAcquisitionData.zpt').__of__(self)
                    123:         return pt()
                    124: 
1.6       dwinter   125: 
1.3       dwinter   126:     def writeAcquisitionMetadata(self,date,path,media_type,producer="mpiwg",description=""):
1.1       dwinter   127:         """Schreibe Acquisiondata in index.meta"""
                    128:         
1.3       dwinter   129:         
                    130:         #schreibe in index.meta
                    131:         subnodes={}
                    132:         subnodes['media-type']=media_type
                    133:         changeNodesInIndexMeta(path,"",subnodes)
                    134:         
                    135:         subnodes={}
                    136:         subnodes['date']=date
                    137:         subnodes['description']=description
                    138:         
                    139:         changeNodesInIndexMeta(path,"acquisition",subnodes)
                    140:         #print "HI"
                    141: 
                    142:         subnodes={}
                    143:         subnodes['provider-id']=producer
                    144:         subnodes['url']=getattr(self.producerFolder,producer).url
                    145:         subnodes['contact']=getattr(self.producerFolder,producer).contact
                    146:         subnodes['address']=getattr(self.producerFolder,producer).address
                    147:         
                    148:         changeNodesInIndexMeta(path,"provider",subnodes,parent="acquisition")
                    149: 
                    150:         
                    151:         self.metaDataSet=self.metaDataSets[self.mediaToAcquisition[media_type]]
                    152:         self.media_type=self.mediaToAcquisition[media_type]
                    153:         
                    154:         pt=PageTemplateFile('Products/OSA_system/inputDocumentMetadata.zpt').__of__(self)
                    155:         return pt()
                    156: 
                    157:         
                    158:     def writeDocumentMetadata(self,referenceType):
                    159: 
                    160:         """write document metadata"""
                    161:         form=self.REQUEST.form
                    162: #schreibe in index.meta
                    163:         self.bibDataSet=self.bibDataSets[form['referenceType']]
                    164:         self.bibdata_type=form['referenceType']
                    165: 
                    166:         subnodes={}
                    167:         subnodes['device']=form['device']
                    168:         subnodes['image-type']=form['image-type']
                    169:         subnodes['production-comment']=form['production-comment']
                    170:         changeNodesInIndexMeta(self.REQUEST.SESSION['path'],"image-acquisition",subnodes)
                    171: 
                    172:         subnodes={}
                    173:         subnodes['dpi']=form['dpi']
                    174: 
                    175:         
                    176:         changeNodesInIndexMeta(self.REQUEST.SESSION['path'],"img",subnodes)
                    177: 
                    178: 
                    179:         pt=PageTemplateFile('Products/OSA_system/inputBiblioMetadata.zpt').__of__(self)
                    180:         return pt()
                    181: 
                    182:     def writeBiblioMetadata(self,bibdata_type,RESPONSE=None):
                    183:         """Write all"""
                    184:         #to do write metadata
                    185: 
                    186:         subnodes={}
                    187:         form=self.REQUEST.form
                    188:         #for key in form.keys():
                    189:         #    subnodes[key]=form['device']
                    190:         subnodes=form
1.6       dwinter   191:         del subnodes['bibdata_type'] #in form but not metadata
1.3       dwinter   192:         changeNodesInIndexMeta(self.REQUEST.SESSION['path'],"bib",subnodes,nodeAttributes={'type':bibdata_type},parent="meta")
1.6       dwinter   193:         #return self.REQUEST
                    194:         return self.addRessourceMeta()
1.3       dwinter   195:         
                    196: 
                    197: 
                    198:     
                    199:         
                    200:     
                    201: 
1.1       dwinter   202: def manage_AddOSAS_metadataOrganizerForm(self):
                    203:     """interface for adding the OSAS_root"""
                    204:     pt=PageTemplateFile('Products/OSA_system/AddOSAS_metadataOrganizer.zpt').__of__(self)
                    205:     return pt()
                    206: 
                    207: 
                    208: def manage_AddOSAS_metadataOrganizer(self,id,title="",RESPONSE=None):
                    209:     """add the OSAS_root"""
                    210:     if title=="":
                    211:         title=id
                    212:         
                    213:     newObj=OSAS_metadataOrganizer(id, title)
                    214:     self._setObject(id,newObj)
                    215:     if RESPONSE is not None:
                    216:         RESPONSE.redirect('manage_main')
                    217:     
                    218: 
                    219: class OSAS_processViewer(SimpleItem,Persistent,Implicit):
                    220:     """Process viewer for archiving"""
                    221: 
                    222:     meta_type="OSAS_processViewer"
                    223: 
                    224:     def __init__(self, id, title):
                    225:         """init"""
                    226:         self.id=id
                    227:         self.title=title
                    228: 
                    229:     def index_html(self):
                    230:         """main page"""
                    231:         pt=PageTemplateFile('Products/OSA_system/processViewerIndex.zpt').__of__(self)
                    232:         return pt()
                    233: 
                    234:     def storeFile(self,something):
                    235:         """store info in session"""
                    236:         self.REQUEST.SESSION['something']=something
                    237:         return 1
                    238: 
1.6       dwinter   239:     def getFile(self,number):
1.1       dwinter   240:         """get info from session"""
1.6       dwinter   241:         check=self.getoverview('/var/tmp/archiver').messages()[number]
                    242:         return check
1.1       dwinter   243: 
                    244:     def getoverview(self,path):
                    245:         """get overview"""
                    246:         return archive.overview(path)
                    247: 
                    248:     def storeerror(self,ret,path,context,i):
                    249:         """store an error"""
                    250:         session=context.REQUEST.SESSION
                    251:         session['error%i'%i]=ret
                    252:         session['path%i'%i]=path
                    253:      
                    254:         return 'error?number=%i'%i
                    255: 
1.4       dwinter   256:     def geterror(self,str,context):
                    257:         session=context.REQUEST.SESSION
                    258:         return session[str]
                    259: 
                    260: 
                    261:     def readfile(self,path):
                    262:         
                    263:         ret=""
                    264:         f=open(path,'r')
                    265:         for g in f.readlines():
                    266:             ret=ret+g
                    267:         return ret
                    268:      
                    269:     def writefile(self,path,txt,REQUEST):
                    270:         f=open(path,'w')
                    271:         f.write(txt)
                    272:         f.close()
                    273:         rval=self.aq_acquire('archive2')
                    274:         return rval()
                    275: 
                    276: 
1.6       dwinter   277:     def view(self,number):
1.1       dwinter   278:         """view page"""
1.6       dwinter   279:         self.errnum=number
1.1       dwinter   280:         pt=PageTemplateFile('Products/OSA_system/processViewerView.zpt').__of__(self)
                    281:         return pt()
1.4       dwinter   282: 
                    283:     def error(self):
                    284:         """view errors"""
                    285:         pt=PageTemplateFile('Products/OSA_system/processViewerError.zpt').__of__(self)
                    286:         return pt()
1.1       dwinter   287:     
                    288: def manage_AddOSAS_processViewerForm(self):
                    289:     """interface for adding the OSAS_processViewer"""
                    290:     pt=PageTemplateFile('Products/OSA_system/AddOSAS_processViewer.zpt').__of__(self)
                    291:     return pt()
                    292: 
                    293: 
                    294: def manage_AddOSAS_processViewer(self,id,title="",RESPONSE=None):
                    295:     """add the OSAS_processViewer"""
                    296:     if title=="":
                    297:         title=id
                    298:         
                    299:     newObj=OSAS_processViewer(id, title)
                    300:     self._setObject(id,newObj)
                    301:     if RESPONSE is not None:
                    302:         RESPONSE.redirect('manage_main')
                    303: 
                    304: 
                    305: 
                    306: class OSAS_archiver(Folder, Persistent,Implicit):
                    307:     """Hauptklasse fuer das Archiv"""
                    308: 
                    309:     meta_type="OSAS_archiver"
                    310: 
1.5       dwinter   311:     manage_options = Folder.manage_options+(
                    312:         {'label':'Main Config','action':'changeOSAS_archiverForm'},
                    313:         )
                    314:      
1.1       dwinter   315:     # to be deleted later
                    316:     #startPath="/mpiwg"
                    317:     ## methoden aus dem alten archive.py
                    318: 
1.3       dwinter   319:    
                    320: 
1.1       dwinter   321:     def archiver(self,path):
                    322:         """archive the documents in path"""
                    323:         tmp=archive.archive(path,self.REQUEST.SESSION)
                    324:         pt=PageTemplateFile('Products/OSA_system/archiveStatus.zpt').__of__(self)
                    325:         return pt()
                    326: 
                    327:         
                    328: 
                    329: 
                    330:     def metachecker(self,path):
                    331:         """check the metadata the documents in path"""
                    332:         self.REQUEST.SESSION['path']=self.REQUEST['path']
                    333:         return archive.metacheck(path)
                    334: 
                    335:     ## methods  from OSAS_show
                    336:     def changeName(self,name):
                    337:         return changeName(name)
                    338: 
                    339:     def hasMetafile(self,path):
                    340:         return hasMetafile(path)
                    341: 
                    342:     def getMetafile(self,path):
                    343:         return getMetafile(path)
                    344: 
                    345:     def toggle_view(self,path,file):
                    346:         """Oeffnen bzw. schließen der Subfolders"""
                    347:         self.tree(path).toggle(path,file)
                    348:         return self.REQUEST.RESPONSE.redirect(self.REQUEST['URL1']+"?path="+path)
                    349: 
                    350: 
                    351: 
                    352:     def isdigilib2(self,path):
                    353:         """check if digilib"""
                    354:         return isdigilib2(path)
                    355: 
                    356:     def path_to_link_view(self,path):
                    357:         """generates navigation bar for viewfiles"""
                    358:         return path_to_link_view(self.REQUEST['URL'],path)
                    359:     
                    360: 
                    361:     def tree(self,start):
                    362:    """get the filetree"""
                    363:    k=browse(start)
                    364:    return k
                    365: 
                    366:     def getfilesystem2(self,start,reload=0):
                    367:    """load filesystem"""
                    368: 
                    369:    k=filesystem2(start,1)
                    370:    return k
                    371: 
                    372:     def getfilesystem(self,start,reload=0):
                    373:    """load filesystem"""
                    374: 
                    375:    k=filesystem(start,1)
                    376:    return k
                    377: 
                    378: 
                    379:     ##init
                    380:     def __init__(self, id, title,startPath):
                    381:         """init"""
                    382:         self.id=id
                    383:         self.title=title
                    384:         self.startPath=startPath
                    385: 
                    386:     def archiver_html(self):
                    387:         """archiver"""
                    388:         pt=PageTemplateFile('Products/OSA_system/OSAS_Archiver.zpt').__of__(self)
                    389:         return pt()
                    390: 
                    391:     def index_html(self):
                    392:         """main page"""
                    393:         pt=PageTemplateFile('Products/OSA_system/archiverIndex.zpt').__of__(self)
                    394:         return pt()
1.3       dwinter   395: 
                    396:     def getDate(self):
                    397:         """date"""
                    398:         return time.strftime("%Y-%m-%d",time.localtime())
1.1       dwinter   399:     
                    400:     def newFolders_html(self):
                    401:         """main page"""
                    402:         pt=PageTemplateFile('Products/OSA_system/newFolders.zpt').__of__(self)
                    403:         return pt()
                    404: 
                    405:     def getProducers(self):
                    406:         """Ausgabe der registrierten Benutzer"""
                    407:         ret=[]
1.3       dwinter   408:         #x=7
                    409:         id=self.producerFolder.getId()
1.1       dwinter   410:         for list in self.producerFolder.__dict__:
                    411:             obj=getattr(self.producerFolder,list)
                    412:             if (hasattr(obj,"meta_type")):
                    413:                 if (obj.meta_type=="OSAS_producer"):
                    414:                     ret.append(obj.getId())
                    415:         return ret
                    416: 
                    417:     def getProducer(self,id):
                    418:         """Gebe ProducerObjekt zurück"""
1.7     ! dwinter   419:         obj=getattr(self.producerFolder,id)
1.1       dwinter   420:         return obj
                    421:         
                    422:         
                    423: 
                    424:     def createFoldersForm(self,producer,number):
                    425:         """Erzeuge Folder im producer Verzeichnis mit ids"""
                    426:         self.REQUEST.SESSION['producer']=producer
                    427:         self.REQUEST.SESSION['ids']=self.idGenerator.giveIdsOut(number)
                    428:         pt=PageTemplateFile('Products/OSA_system/createFoldersForm.zpt').__of__(self)
                    429:         return pt()
                    430:     
                    431:     def createFolders(self,folderList,producer):
                    432:         """Erzeug die entsprechenden Folder"""
1.5       dwinter   433:         #hack
1.7     ! dwinter   434:         #producer="library"
1.5       dwinter   435:         if type(folderList)==StringType:
                    436:             folders=[folderList]
                    437:         else:
                    438:             folders=folderList
1.7     ! dwinter   439:         #return producer
        !           440:         producerFolderName=self.getProducer(producer).producerFolderName
1.5       dwinter   441:         
                    442:         for folder in folders:
1.7     ! dwinter   443:             os.mkdir(self.startPath+"/"+producerFolderName+"/"+folder)
        !           444:             os.chmod(self.startPath+"/"+producerFolderName+"/"+folder,0775)
1.5       dwinter   445:         self.REQUEST.SESSION['folderList']=folders
1.1       dwinter   446:         pt=PageTemplateFile('Products/OSA_system/createFolders.zpt').__of__(self)
                    447:         return pt()
                    448: 
1.4       dwinter   449:     def storeerror(self,ret,path,context,i):
                    450:         """store an error"""
                    451:         session=context.REQUEST.SESSION
                    452:         session['error%i'%i]=ret
                    453:         session['path%i'%i]=path
                    454:      
                    455:         return 'error?number=%i'%i
                    456: 
                    457:     def geterror(self,str,context):
                    458:         session=context.REQUEST.SESSION
                    459:         return session[str]
                    460: 
                    461:     def readfile(self,path):
                    462:         
                    463:         ret=""
                    464:         f=open(path,'r')
                    465:         for g in f.readlines():
                    466:             ret=ret+g
                    467:         return ret
                    468:      
                    469:     def writefile(self,path,txt,REQUEST):
                    470:         f=open(path,'w')
                    471:         f.write(txt)
                    472:         f.close()
                    473:         rval=self.aq_acquire('archive2')
                    474:         return rval()
                    475: 
                    476:     def error(self):
                    477:         """view errors"""
                    478:         pt=PageTemplateFile('Products/OSA_system/processViewerError.zpt').__of__(self)
                    479:         return pt()
                    480: 
                    481:     
1.1       dwinter   482:     def archiveSelected(self):
                    483:         """Archiviere ausgewaehlte files"""
                    484:         pt=PageTemplateFile('Products/OSA_system/archiveSelected.zpt').__of__(self)
                    485:         return pt()
                    486: 
                    487:     def enterAcquisitionMetadata(self):
                    488:         """Erstelle Metadaten fuer Acquisition"""
                    489: 
                    490:     def enterPreliminaryBibMeta(self):
                    491:         """Erstelle Metadaten fuer Bibliography"""
                    492:         
                    493:     def showFilesForArchiving(self):
                    494:         """Anzeige der noch zu archivieren Files"""
                    495: 
                    496:         
1.5       dwinter   497:     def changeOSAS_archiverForm(self):
                    498:         """change"""
                    499:         pt=PageTemplateFile('Products/OSA_system/ChangeOSAS_archiver.zpt').__of__(self)
                    500:         return pt()
                    501: 
                    502:     def changeOSAS_archiver(self,startPath,title="",RESPONSE=None):
                    503:         """change"""
                    504:         self.startPath=startPath
                    505:         self.title=title
1.1       dwinter   506: 
1.5       dwinter   507:         if RESPONSE is not None:
                    508:             RESPONSE.redirect('manage_main')
1.1       dwinter   509:     
                    510: 
                    511: def manage_AddOSAS_archiverForm(self):
                    512:     """interface for adding the OSAS_root"""
                    513:     pt=PageTemplateFile('Products/OSA_system/AddOSAS_archiver.zpt').__of__(self)
                    514:     return pt()
                    515: 
                    516: 
                    517: def manage_AddOSAS_archiver(self,id,startPath,title="",RESPONSE=None):
                    518:     """add the OSAS_root"""
                    519:     if title=="":
                    520:         title=id
                    521:         
                    522:     newObj=OSAS_archiver(id, title,startPath)
                    523:     self._setObject(id,newObj)
                    524:     if RESPONSE is not None:
                    525:         RESPONSE.redirect('manage_main')
                    526: 
                    527: 
1.3       dwinter   528: class OSAS_producer(SimpleItem,Persistent,Implicit):
1.2       dwinter   529:     """Klasse fuer Produzenteninformationen
                    530:     Metadaten nach  V1.1.1"""
1.1       dwinter   531: 
                    532:     meta_type="OSAS_producer"
                    533: 
1.7     ! dwinter   534:     def __init__(self,shortName,fullName,producerFolderName,address="",url="",contact=""):
1.1       dwinter   535: 
                    536:         self.id=shortName
                    537:         self.title=fullName
1.2       dwinter   538:         self.address=address
                    539:         self.url=url
                    540:         self.contact=contact
1.7     ! dwinter   541:         self.producerFolderName=producerFolderName
        !           542:         
1.3       dwinter   543:     manage_options = SimpleItem.manage_options+(
1.2       dwinter   544:         {'label':'Main Config','action':'changeOSAS_producerForm'},
                    545:         )
                    546: 
                    547:     def changeOSAS_producerForm(self):
                    548:         """change"""
                    549:         pt=PageTemplateFile('Products/OSA_system/ChangeOSAS_producer.zpt').__of__(self)
                    550:         return pt()
1.3       dwinter   551: 
1.7     ! dwinter   552:     def changeOSAS_producer(self,title,address,producerFolderName,contact="",url="",RESPONSE=None):
1.3       dwinter   553:         """change"""
1.7     ! dwinter   554:         self.title=title
1.3       dwinter   555:         self.address=address
                    556:         self.url=url
                    557:         self.contact=contact
1.7     ! dwinter   558:         self.producerFolderName=producerFolderName
1.1       dwinter   559: 
1.7     ! dwinter   560:         if RESPONSE:
        !           561:             RESPONSE.redirect("manage_main")
        !           562:         
1.1       dwinter   563: def manage_AddOSAS_producerForm(self):
                    564:     """interface for adding the OSAS_root"""
                    565:     pt=PageTemplateFile('Products/OSA_system/AddOSAS_producer.zpt').__of__(self)
                    566:     return pt()
                    567: 
                    568: 
1.7     ! dwinter   569: def manage_AddOSAS_producer(self,id,producerFolderName,title="",contact="",address="",url="",RESPONSE=None):
1.1       dwinter   570:     """add the OSAS_root"""
                    571:     if title=="":
                    572:         title=id
                    573:         
1.7     ! dwinter   574:     newObj=OSAS_producer(id, title,producerFolderName,address,contact,url)
1.1       dwinter   575:     self._setObject(id,newObj)
                    576:     if RESPONSE is not None:
                    577:         RESPONSE.redirect('manage_main')
                    578: 

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>