打造个人公告板与文件共享系统
个人公告板系统
在开发个人公告板系统时,为了让用户能方便地发布、查看和回复消息,我们采用了一个脚本对应一个用户操作的结构,具体脚本及功能如下表所示:
| 脚本名称 | 功能描述 |
| — | — |
| 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
通过这个流程图,我们可以清晰地看到整个开发过程的步骤和决策点,有助于我们更好地规划和管理项目。
超级会员免费看
52

被折叠的 条评论
为什么被折叠?



