39、打造个人公告板与文件共享系统

打造个人公告板与文件共享系统

个人公告板系统

在开发个人公告板系统时,为了让用户能方便地发布、查看和回复消息,我们采用了一个脚本对应一个用户操作的结构,具体脚本及功能如下表所示:
| 脚本名称 | 功能描述 |
| — | — |
| main.cgi | 显示所有消息的主题(按线程排列),并提供文章链接,同时底部有发布新消息的链接 |
| view.cgi | 显示单篇文章,包含返回主页面和回复文章的链接 |
| edit.cgi | 以可编辑形式显示文章,提交按钮链接到保存脚本 |
| save.cgi | 接收文章信息并保存到数据库表中 |

下面是各个脚本的详细介绍及代码实现:

main.cgi

该脚本与第一个原型中的 simple_main.cgi 类似,但增加了链接。每个主题都是指向 view.cgi 的链接,页面底部还有指向 edit.cgi 的“Post message”链接。

#!/usr/bin/python
print 'Content-type: text/html\n'
import cgitb; cgitb.enable()
import psycopg
conn = psycopg.connect('dbname=foo user=bar')
curs = conn.cursor()
print """
<html>
  <head>
    <title>The FooBar Bulletin Board</title>
  </head>
  <body>
    <h1>The FooBar Bulletin Board</h1>
    """
curs.execute('SELECT * FROM messages')
rows = curs.dictfetchall()
toplevel = []
children = {}
for row in rows:
    parent_id = row['reply_to']
    if parent_id is None:
        toplevel.append(row)
    else:
        children.setdefault(parent_id,[]).append(row)
def format(row):
    print '<p><a href="view.cgi?id=%(id)i">%(subject)s</a></p>' % row
    try: kids = children[row['id']]
    except KeyError: pass
    else:
        print '<blockquote>'
        for kid in kids:
            format(kid)
        print '</blockquote>'
print '<p>'
for row in toplevel:
    format(row)
print """
    </p>
    <hr />
    <p><a href="edit.cgi">Post message</a></p>
  </body>
</html>
"""
view.cgi

此脚本使用 CGI 参数 id 从数据库中检索单条消息,并格式化显示在 HTML 页面上。页面包含返回主页面和回复文章的链接。

#!/usr/bin/python
print 'Content-type: text/html\n'
import cgitb; cgitb.enable()
import psycopg
conn = psycopg.connect('dbname=foo user=bar')
curs = conn.cursor()
import cgi, sys
form = cgi.FieldStorage()
id = form.getvalue('id')
print """
<html>
  <head>
    <title>View Message</title>
  </head>
  <body>
    <h1>View Message</h1>
    """
try: id = int(id)
except:
    print 'Invalid message ID'
    sys.exit()
curs.execute('SELECT * FROM messages WHERE id = %i' % id)
rows = curs.dictfetchall()
if not rows:
    print 'Unknown message ID'
    sys.exit()
row = rows[0]
print """
    <p><b>Subject:</b> %(subject)s<br />
    <b>Sender:</b> %(sender)s<br />
    <pre>%(text)s</pre>
    </p>
    <hr />
    <a href='main.cgi'>Back to the main page</a>
    | <a href="edit.cgi?reply_to=%(id)s">Reply</a>
  </body>
</html>
""" % row
edit.cgi

该脚本具有双重功能,可用于编辑新消息和回复。若 CGI 请求中提供了 reply_to ,会在编辑表单中以隐藏输入的形式保留该信息,并且默认主题会设置为“Re: parentsubject”。

#!/usr/bin/python
print 'Content-type: text/html\n'
import cgitb; cgitb.enable()
import psycopg
conn = psycopg.connect('dbname=foo user=bar')
curs = conn.cursor()
import cgi, sys
form = cgi.FieldStorage()
reply_to = form.getvalue('reply_to')
print """
<html>
  <head>
    <title>Compose Message</title>
  </head>
  <body>
    <h1>Compose Message</h1>
    <form action='save.cgi' method='POST'>
    """
subject = ''
if reply_to is not None:
    print '<input type="hidden" name="reply_to" value="%s"/>' % reply_to
    curs.execute('SELECT subject FROM messages WHERE id = %s' % reply_to)
    subject = curs.fetchone()[0]
    if not subject.startswith('Re: '):
        subject = 'Re: ' + subject
print """
    <b>Subject:</b><br />
    <input type='text' size='40' name='subject' value='%s' /><br />
    <b>Sender:</b><br />
    <input type='text' size='40' name='sender' /><br />
    <b>Message:</b><br />
    <textarea name='text' cols='40' rows='20'></textarea><br />
    <input type='submit' value='Save'/>
    </form>
    <hr />
    <a href='main.cgi'>Back to the main page</a>'
  </body>
</html>
""" % subject
save.cgi

该脚本接收来自 edit.cgi 的文章信息,并使用 SQL 插入命令将其保存到数据库中,同时调用 conn.commit() 确保数据不丢失。

#!/usr/bin/python
print 'Content-type: text/html\n'
import cgitb; cgitb.enable()
def quote(string):
    if string:
        return string.replace("'", "\\'")
    else:
        return string
import psycopg
conn = psycopg.connect('dbname=foo user=bar')
curs = conn.cursor()
import cgi, sys
form = cgi.FieldStorage()
sender = quote(form.getvalue('sender'))
subject = quote(form.getvalue('subject'))
text = quote(form.getvalue('text'))
reply_to = form.getvalue('reply_to')
if not (sender and subject and text):
    print 'Please supply sender, subject, and text'
    sys.exit()
if reply_to is not None:
    query = """
    INSERT INTO messages(reply_to, sender, subject, text)
    VALUES(%i, '%s', '%s', '%s')""" % (int(reply_to), sender, subject, text)
else:
    query = """
    INSERT INTO messages(sender, subject, text)
    VALUES('%s', '%s', '%s')""" % (sender, subject, text)
curs.execute(query)
conn.commit()
print """
<html>
  <head>
    <title>Message Saved</title>
  </head>
  <body>
    <h1>Message Saved</h1>
    <hr />
    <a href='main.cgi'>Back to the main page</a>
  </body>
</html>s
"""
测试公告板系统

测试该系统的步骤如下:
1. 打开 main.cgi ,点击“Post message”链接,进入 edit.cgi
2. 在各个字段输入值,点击“Save”链接,进入 save.cgi ,会显示“Message Saved”。
3. 点击“Back to the main page”链接返回主页面,此时列表应包含新发布的消息。
4. 点击消息主题,进入 view.cgi ,点击“Reply”链接,再次进入 edit.cgi ,输入内容并保存,返回主页面查看回复。

文件共享系统

我们要开发一个基于 XML - RPC 的简单对等文件共享程序,该程序的目标是让不同机器上的程序能够交换文件。为了实现这个目标,程序需要满足以下具体要求:
- 每个节点要记录一组已知节点,以便请求帮助,并且节点之间可以相互介绍。
- 可以向节点请求文件,若节点没有则向邻居节点依次请求。
- 查询节点时要提供历史记录,避免循环查询和过长的查询链。
- 能够连接到节点并以可信方身份访问特定功能,如让节点从网络中下载和存储文件。
- 要有用户界面,方便连接节点并让其下载文件,且界面易于扩展和替换。

下面是实现该系统的详细步骤:

准备工作

需要确保安装了 xmlrpclib SimpleXMLRPCServer 库,并且使用的是更新版本的 SimpleXMLRPCServer 以避免安全问题。虽然不连接网络也可使用该软件,但连接网络会更有趣。可以在多台连接的机器上运行,也可在同一台机器上运行多个文件共享节点进行测试。

实现思路

在编写 Node 类的第一个原型之前,需要了解 SimpleXMLRPCServer 类的工作方式。它通过元组 (servername, port) 进行实例化,其中 servername 是服务器运行的机器名,可使用空字符串表示本地主机,端口号通常为 1024 及以上。实例化后,可以使用 register_instance 方法注册实现“远程方法”的实例,或使用 register_function 方法注册单个函数。准备好运行服务器时,调用 serve_forever 方法。

以下是一个简单的测试示例:

# 第一个解释器
from SimpleXMLRPCServer import SimpleXMLRPCServer
s = SimpleXMLRPCServer(("", 4242)) # Localhost at port 4242
def twice(x): 
    return x*2
s.register_function(twice) 
s.serve_forever() 

# 第二个解释器
from xmlrpclib import ServerProxy 
s = ServerProxy('http://localhost:4242') 
print(s.twice(2))
Node 类的实现

Node 类需要包含以下属性和方法:
- 属性
- url :节点的 URL
- dirname :存储文件的目录名
- secret :用于身份验证的“秘密”(密码)
- known :已知节点的 URL 集合

  • 方法
  • query :查询文件,可能会向邻居节点请求帮助
  • fetch :在秘密验证通过后,执行查询并存储文件
  • hello :将其他节点添加到已知节点集合中

以下是 Node 类的完整代码实现:

from xmlrpclib import ServerProxy
from os.path import join, isfile
from SimpleXMLRPCServer import SimpleXMLRPCServer
from urlparse import urlparse
import sys
MAX_HISTORY_LENGTH = 6
OK = 1
FAIL = 2
EMPTY = ''
def getPort(url):
    'Extracts the port from a URL'
    name = urlparse(url)[1]
    parts = name.split(':')
    return int(parts[-1])

class Node:
    """
    A node in a peer-to-peer network.
    """
    def __init__(self, url, dirname, secret):
        self.url = url
        self.dirname = dirname
        self.secret = secret
        self.known = set()
    def query(self, query, history=[]):
        """
        Performs a query for a file, possibly asking other known Nodes for
        help. Returns the file as a string.
        """
        code, data = self._handle(query)
        if code == OK:
            return code, data
        else:
            history = history + [self.url]
            if len(history) >= MAX_HISTORY_LENGTH:
                return FAIL, EMPTY
            return self._broadcast(query, history)
    def hello(self, other):
        """
        Used to introduce the Node to other Nodes.
        """
        self.known.add(other)
        return OK
    def fetch(self, query, secret):
        """
        Used to make the Node find a file and download it.
        """
        if secret != self.secret: return FAIL
        code, data = self.query(query)
        if code == OK:
            f = open(join(self.dirname, query), 'w')
            f.write(data)
            f.close()
            return OK
        else:
            return FAIL
    def _start(self):
        """
        Used internally to start the XML-RPC server.
        """
        s = SimpleXMLRPCServer(("", getPort(self.url)), logRequests=False)
        s.register_instance(self)
        s.serve_forever()
    def _handle(self, query):
        """
        Used internally to handle queries.
        """
        dir = self.dirname
        name = join(dir, query)
        if not isfile(name): return FAIL, EMPTY
        return OK, open(name).read()
    def _broadcast(self, query, history):
        """
        Used internally to broadcast a query to all known Nodes.
        """
        for other in self.known.copy():
            if other in history: continue
            try:
                s = ServerProxy(other)
                code, data = s.query(query, history)
                if code == OK:
                    return code, data
            except:
                self.known.remove(other)
        return FAIL, EMPTY
def main():
    url, directory, secret = sys.argv[1:]
    n = Node(url, directory, secret)
    n._start()
if __name__ == '__main__': main()
测试文件共享系统

以下是一个简单的测试示例:
1. 打开多个终端,创建两个目录 files1 files2 ,将 test.txt 文件放入 files2 目录。
2. 在一个终端运行: python simple_node.py http://localhost:4242 files1 secret1
3. 在另一个终端运行: python simple_node.py http://localhost:4243 files2 secret2
4. 启动交互式 Python 解释器,进行以下操作:

from xmlrpclib import *
mypeer = ServerProxy('http://localhost:4242') 
code, data = mypeer.query('test.txt')
print(code) 
otherpeer = ServerProxy('http://localhost:4243') 
code, data = otherpeer.query('test.txt')
print(code) 
print(data) 
mypeer.hello('http://localhost:4243') 
print(mypeer.query('test.txt')) 
print(mypeer.fetch('test.txt', 'secret1')) 

通过上述步骤,我们可以看到文件共享系统的基本功能得到了实现,第一个节点在引入第二个节点后能够成功获取文件并下载存储。

整个系统的工作流程可以用以下 mermaid 流程图表示:

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;

    A([开始]):::startend --> B(启动 main.cgi):::process
    B --> C{选择操作}:::decision
    C -->|发布新消息| D(进入 edit.cgi):::process
    C -->|查看文章| E(进入 view.cgi):::process
    D --> E1(输入文章信息):::process
    E1 --> F(点击保存,进入 save.cgi):::process
    F --> G(保存文章到数据库):::process
    G --> H(显示消息保存成功):::process
    H --> I(返回 main.cgi):::process
    E --> J{选择操作}:::decision
    J -->|回复文章| K(进入 edit.cgi 并设置 reply_to):::process
    K --> E1
    J -->|返回主页面| I
    I --> L(显示更新后的消息列表):::process
    L --> M{选择文件共享操作}:::decision
    M -->|启动节点| N(运行 simple_node.py):::process
    N --> O(节点初始化):::process
    O --> P{选择操作}:::decision
    P -->|查询文件| Q(调用 query 方法):::process
    P -->|引入节点| R(调用 hello 方法):::process
    P -->|下载文件| S(调用 fetch 方法):::process
    Q --> T{文件是否存在}:::decision
    T -->|是| U(返回文件内容):::process
    T -->|否| V(向邻居节点请求):::process
    V --> W{邻居节点是否有文件}:::decision
    W -->|是| U
    W -->|否| X(返回失败信息):::process
    R --> Y(更新已知节点集合):::process
    S --> Z{秘密是否正确}:::decision
    Z -->|是| Q
    Z -->|否| X
    U --> AA(显示文件内容):::process
    AA --> AB(可选择保存文件):::process
    AB --> AC(保存文件到本地目录):::process
    X --> AD(显示失败提示):::process
    Y --> P
    AC --> P
    AD --> P

通过以上的介绍和实现,我们完成了个人公告板和文件共享系统的开发和测试,并且可以根据需求进行进一步的扩展和优化。

打造个人公告板与文件共享系统(续)

进一步探索

现在我们已经掌握了开发强大 Web 应用和文件共享系统的能力,有很多方向值得我们深入探索:

开发特定数据库的 Web 前端

可以尝试为自己喜欢的 Monty Python 草图数据库创建一个 Web 前端。具体操作步骤如下:
1. 设计数据库结构,确定需要存储的信息,如草图名称、描述、演员等。
2. 使用合适的数据库管理系统(如 PostgreSQL、MySQL 等)创建数据库和相应的表。
3. 编写 CGI 脚本,实现对数据库的查询、插入、更新和删除操作。例如,可以创建一个 search.cgi 脚本来根据关键词搜索草图信息。
4. 设计 HTML 页面,将查询结果以友好的方式展示给用户。

优化现有系统

如果想改进当前的公告板和文件共享系统,可以考虑以下几点:
- 抽象代码 :创建一个实用模块,包含打印标准头部和标准页脚的函数。这样可以避免在每个脚本中重复编写相同的 HTML 代码。例如:

# utils.py
def print_header(title):
    print """
<html>
  <head>
    <title>%s</title>
  </head>
  <body>
    <h1>%s</h1>
    """ % (title, title)

def print_footer():
    print """
  </body>
</html>
    """

然后在各个脚本中导入并使用这些函数:

# main.cgi
#!/usr/bin/python
print 'Content-type: text/html\n'
import cgitb; cgitb.enable()
import psycopg
from utils import print_header, print_footer
conn = psycopg.connect('dbname=foo user=bar')
curs = conn.cursor()
print_header('The FooBar Bulletin Board')
# 其他代码...
print_footer()
  • 用户数据库和密码处理 :添加一个用户数据库,实现密码验证功能,增强系统的安全性。可以使用哈希算法(如 SHA - 256)对用户密码进行加密存储。
  • 抽象数据库连接代码 :将创建数据库连接的代码封装成一个函数,避免在每个脚本中重复编写。例如:
# db_utils.py
import psycopg

def get_db_connection():
    return psycopg.connect('dbname=foo user=bar')

在脚本中使用:

# main.cgi
#!/usr/bin/python
print 'Content-type: text/html\n'
import cgitb; cgitb.enable()
from db_utils import get_db_connection
conn = get_db_connection()
curs = conn.cursor()
# 其他代码...
选择不同的存储解决方案

如果不想使用专门的服务器来存储数据,可以考虑以下几种选择:
| 存储方案 | 特点 | 链接 |
| — | — | — |
| SQLite | 轻量级数据库,无需服务器,可直接在文件中存储数据 | |
| Metakit | 小巧的数据库包,可将整个数据库存储在单个文件中 | http://equi4.com/metakit/python.html |
| Berkeley DB | 简单且能高效处理大量数据 | http://www.sleepycat.com |

总结

通过本文,我们详细介绍了个人公告板系统和文件共享系统的开发过程。个人公告板系统采用了一个脚本对应一个用户操作的结构,包括 main.cgi view.cgi edit.cgi save.cgi 脚本,实现了消息的发布、查看和回复功能。文件共享系统基于 XML - RPC 技术,满足了节点间文件交换的需求,通过 Node 类实现了文件查询、引入节点和下载文件等功能。

在开发过程中,我们还学习了如何测试系统,以及如何对系统进行进一步的优化和扩展。同时,我们也介绍了不同的存储解决方案,为系统的存储需求提供了更多选择。

希望这些内容能帮助你更好地理解和开发类似的 Web 应用和文件共享系统,你可以根据自己的需求对系统进行定制和改进,打造出更强大、更实用的工具。

以下是一个总结系统操作流程的 mermaid 流程图:

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;

    A([开始开发]):::startend --> B(设计公告板系统结构):::process
    B --> C(编写脚本:main.cgi、view.cgi 等):::process
    C --> D(测试公告板系统):::process
    D --> E{公告板系统是否正常}:::decision
    E -->|是| F(设计文件共享系统结构):::process
    E -->|否| C
    F --> G(实现 Node 类):::process
    G --> H(编写文件共享脚本):::process
    H --> I(测试文件共享系统):::process
    I --> J{文件共享系统是否正常}:::decision
    J -->|是| K(进行系统优化和扩展):::process
    J -->|否| H
    K --> L(选择存储解决方案):::process
    L --> M(部署系统):::process
    M --> N([完成开发]):::startend

通过这个流程图,我们可以清晰地看到整个开发过程的步骤和决策点,有助于我们更好地规划和管理项目。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值