python day 18: thinking in UML与FTP作业重写
python day 18
2019/10/29
1. thinking in UML读书小感
这3天在看谭云杰的thinking in UML这本书,500多页的PDF扫描版,现在只看到279页,算是看完了一半,很多概念都是半懂不懂的,如在云山雾罩中一样。虽然看得不太明白,但是也有一些小感悟。
- 代码并不是全部,前期的需求分析,建模设计才是重点,这个就像行军打仗做好作战方略,备好粮草一样,后面的代码就是排兵步阵了。
- UML能看懂,不代表会画,会画的人必定是懂得RUP的人,这也解释了我这个初学者连一个小小的多用户登录FTP的程序的用例图都没画好的原因。
- 目标问题,我的目标是学会python,先掌握一门语言,而不是先上来就更高级的系统分析,有点好高骛远了。
- 不过,这本书看到,然后现在停下来,还是对我有不小的收获,对于整个软件开发的全貌有了不同的认识。同时也理解为什么很多公司不愿意招培训班或者自学的人了,因为如果只会写代码,像一些沟通用图如UML没有掌握,团队之间就不好沟通。
2. FTP作业重写
2.1 软件目录结构
按照老师的讲解,在命令行模式下输入python FTPServer.py start
.
另一个命令行输入python FTPClient.py -s 127.0.0.1 -p 9999
。
2.2 FTPClient端脚本
2.2.1 bin目录下的FTPClient.py模块
import os
import sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from lib import client
if __name__ == '__main__':
client.Client(sys.argv)
2.2.2 config目录下的settings.py模块
import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
USER_HOME = os.path.join(BASE_DIR, "db", "users")
2.2.3 lib目录下的client.py模块
import os
import sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import hashlib
import json
import socket
from config import settings
import getpass
import time
class Client(object):
def __init__(self, sys_argv):
self.USER_HOME = settings.USER_HOME
self.cwd = ""
self.args = sys_argv
self.HOST_IP = None
self.HOST_PORT = None
self.sock = None
self.logout_flag = False
self.response_code_dict = {
"100": "user successfully registerd",
"101": "username already existed,enter another username",
"200": "pass users authentication",
"201": "wrong username or password",
"202": "user does not exist",
"300": "ready to get file from server",
"301": "ready to send to server",
"302": "file doesn't exist on ftp server",
"303": "storage is full",
"601": "changed directory",
"602": "failed to find directory",
"2003": "already existed",
"2004": 'continue put',
"2005": "directory created"
}
self.argv_parse()
def argv_parse(self):
if len(self.args) < 5:
self.help_msg()
sys.exit()
else:
mandatory_fields = ['-s', '-p']
for i in mandatory_fields:
if i not in self.args:
self.help_msg()
sys.exit()
try:
self.HOST_IP = self.args[self.args.index('-s') + 1]
self.HOST_PORT = int(self.args[self.args.index('-p') + 1])
self.handle()
except (IndexError, ValueError):
# 如果有索引错误,就打印帮助信息并退出程序
self.help_msg()
sys.exit("hhhh")
def help_msg(self):
msg = """
input like below:\n
python FTPClient.py -s 127.0.0.1 -p 9999
"""
print(msg)
def handle(self):
self.connect(self.HOST_IP, self.HOST_PORT)
while True:
username = input("username:>>>").strip()
password = getpass.getpass("password:>>>").strip()
md5 = hashlib.md5("lan".encode("utf-8"))
md5.update(password.encode("utf-8"))
password = md5.hexdigest()
user_pwd_dict = {"username": username, "password": password}
user_pwd = json.dumps(user_pwd_dict)
inp = input("请输入数字:1是登录,2是注册(q退出):>>>").strip()
if len(inp) < 1:
continue
if inp == '1':
if self.auth(user_pwd):
self.interactive()
elif inp == '2':
self.register(user_pwd)
elif inp.lower() == 'q':
break
else:
print("Invalid input")
self.sock.close()
def connect(self, ip, port):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((ip, port,))
def auth(self, user_pwd):
'''
验证用户名与密码
:param user_pwd:
:return:
'''
self.sock.sendall(("user_auth|%s" % user_pwd).encode("utf-8"))
data = json.loads(self.sock.recv(1024).decode("utf-8"))
if data["result"] == "200":
self.username = data["username"]
self.cwd = data["user_home"]
self.storage_limit = data["storage_limit"]
self.storage_used = data["storage_used"]
return True
elif data["result"] == "202":
print(self.response_code_dict["202"])
return False
else:
print(self.response_code_dict["201"])
return False
def register(self, user_pwd):
'''
注册用户
:param user_pwd:
:return:
'''
self.sock.sendall(("user_register|%s" % user_pwd).encode("utf-8"))
msg = self.sock.recv(1024)
if msg == b"100":
print(self.response_code_dict["100"])
elif msg == b"101":
print(self.response_code_dict["101"])
def interactive(self):
while not self.logout_flag:
cmd = input("[%s %s]:" % (self.username, self.cwd)).strip()
if len(cmd) < 1: continue
cmd_str = "cmd_" + cmd.split()[0]
if hasattr(self, cmd_str):
func = getattr(self, cmd_str)
func(cmd)
else:
print("Invalid command")
def cmd_cd(self, cmd):
'''
切换路径
:param cmd:
:return:
'''
if len(cmd.split()) < 2: pass
input_path = cmd.split()[1].strip()
print("try_path:>>>", input_path)
dir_list = self.cwd.split(os.sep)
print("dir_list:>>>", dir_list) # ["lanxing",""]
if dir_list[-1] == "":
del dir_list[-1]
new_path = ""
if input_path == "..":
del dir_list[-1] # []
if len(dir_list) > 0:
new_path = os.sep.join(dir_list) + os.sep
else:
if input_path.startswith(self.cwd):
new_path = input_path
else:
new_path = os.path.join(self.cwd, input_path) + os.sep
print("new_path:>>>", new_path)
if not new_path.startswith(self.username):
pass
else:
data = {"cwd": new_path}
data_json = json.dumps(data)
self.sock.sendall(("cd|%s" % data_json).encode("utf-8"))
server_reply = self.sock.recv(1024)
auth_result = json.loads(server_reply.decode("utf-8"))
if auth_result["result"] == "601":
self.cwd = new_path
else:
print(self.response_code_dict["602"])
def cmd_ls(self, cmd):
msg = ""
if len(cmd.split()) == 1:
msg = json.dumps({"cwd": self.cwd})
elif len(cmd.split()) == 2:
path = cmd.split()[1]
dir_list = self.cwd.split()
new_path = ""
if dir_list[-1] == "":
del dir_list[-1]
if path == "..":
del dir_list[-1]
if len(dir_list) > 0:
new_path = os.sep.join(dir_list) + os.sep
else:
if path.startswith(self.user_def_cwd):
new_path = path
else:
new_path = self.user_def_cwd + path
if not new_path.startswith(self.user_def_cwd):
pass
msg = json.dumps({"cwd": new_path})
self.sock.sendall(("ls|{0}".format(msg)).encode("utf-8"))
msg_size = int(self.sock.recv(1024).decode("utf-8"))
self.sock.sendall(b"300")
has_received = 0
auth_result = ""
while has_received < msg_size:
data = self.sock.recv(1024)
auth_result += data.decode("utf-8")
has_received += len(data)
auth_result = json.loads(auth_result)
if auth_result["result"]:
print(auth_result["result"])
else:
print(self.response_code_dict["302"])
def cmd_put(self, cmd):
data = {"file_size": None, "file_name": None, "dst_path": None}
if len(cmd.split()) == 2:
src_file = cmd.split()[1]
dst_path = self.cwd
else:
src_file, dst_path = cmd.split()[1], cmd.split()[2]
if len(src_file.split(os.sep)) == 1:
src_file = os.path.join(settings.BASE_DIR, "bin", src_file)
if os.path.isfile(src_file):
file_size = os.stat(src_file).st_size
data["file_size"] = file_size
data["file_name"] = os.path.basename(src_file)
if dst_path.startswith("lanxing"):
data["dst_path"] = dst_path
else:
print("Wrong directory!")
data_json = json.dumps(data)
self.sock.sendall(("put|%s" % data_json).encode("utf-8"))
auth_result = json.loads(self.sock.recv(1024).decode("utf-8"))
print(auth_result)
has_sent = 0
with open(src_file, "rb") as f:
if auth_result["result"] == "2003":
print(self.response_code_dict["2003"], )
print("服务端同名文件大小:%s,本地文件大小:%s" % (auth_result["file_size"], file_size))
choice = input("Y:续传;N:覆盖 >>>").strip()
if choice.upper() == "Y":
f.seek(auth_result["file_size"])
self.sock.sendall(b"2004")
has_sent += auth_result["file_size"]
elif choice.upper() == "N":
self.sock.sendall(b"301")
elif auth_result["result"] == "302":
self.sock.sendall(b"301")
print(self.sock.recv(1024))
for line in f:
self.sock.sendall(line)
has_sent += len(line)
percent = has_sent / file_size * 100
sys.stdout.write("\r")
sys.stdout.write("%.2f%% |%s" % (percent, int(percent) * "*"))
sys.stdout.flush()
time.sleep(0.01)
else:
print("file does not exist!")
def cmd_get(self, cmd):
client_path = ""
data = {
"file_name": None,
"client_path": None,
"file_size": None,
"result":"300"
}
if len(cmd.split()) > 1:
file_path = cmd.split()[1]
file_name = os.path.basename(file_path)
if file_path.startswith(self.username):
client_path = file_path
else:
client_path = os.path.join(self.cwd, file_path)
if len(cmd.split()) == 2:
dst_path = os.path.join(settings.USER_HOME, self.cwd, file_name)
elif len(cmd.split()) == 3:
dst_path = cmd.split()[2]
if not dst_path.startswith(self.username):
dst_path = os.path.join(settings.USER_HOME, self.cwd, dst_path, file_name)
else:
dst_path = os.path.join(settings.USER_HOME, dst_path, file_name)
if os.path.exists(dst_path):
file_size = os.stat(dst_path).st_size
data["file_size"] = file_size
data["client_path"] = client_path
data_json = json.dumps(data)
self.sock.sendall(("get|%s" % data_json).encode("utf-8"))
server_reply = json.loads(self.sock.recv(1024).decode("utf-8"))
has_received = 0
if server_reply["result"]=="2003":
choice=input("目标文件已存在,续载Y或全部重新下载N:").strip()
if choice.upper() =="Y":
data["result"]="2004"
data_json = json.dumps(data).encode("utf-8")
self.sock.sendall(data_json)
has_received += file_size
try:
os.makedirs(os.path.dirname(dst_path))
except OSError:
pass
with open(dst_path,"ab") as f:
while has_received < server_reply["file_size"]:
ret = self.sock.recv(1024)
f.write(ret)
has_received += len(ret)
percent = has_received/server_reply["file_size"]*100
sys.stdout.write("\r")
sys.stdout.write("%.2f%%"%percent)
sys.stdout.flush()
elif choice.upper()=="N":
data["result"] = "300"
data_json = json.dumps(data).encode("utf-8")
self.sock.sendall(data_json)
try:
os.makedirs(os.path.dirname(dst_path))
except OSError:
pass
with open(dst_path, "wb") as f:
while has_received < server_reply["file_size"]:
ret = self.sock.recv(1024)
f.write(ret)
has_received += len(ret)
percent = has_received / server_reply["file_size"] * 100
sys.stdout.write("\r")
sys.stdout.write("%.2f%%" % percent)
sys.stdout.flush()
elif server_reply["result"]=="300":
data["result"] = "300"
data_json = json.dumps(data).encode("utf-8")
self.sock.sendall(data_json)
try:
os.makedirs(os.path.dirname(dst_path))
except OSError:
pass
with open(dst_path, "wb") as f:
while has_received < server_reply["file_size"]:
ret = self.sock.recv(1024)
f.write(ret)
has_received += len(ret)
percent = has_received / server_reply["file_size"] * 100
sys.stdout.write("\r")
sys.stdout.write("%.2f%%" % percent)
sys.stdout.flush()
else:
print("Wrong instructions")
def cmd_mkdir(self, cmd):
new_path = ""
if len(cmd.split()) <= 1:
pass
elif len(cmd.split()) == 2:
input_path = cmd.split()[1]
if input_path.startswith(self.cwd):
new_path = input_path
else:
new_path = os.path.join(self.cwd, input_path)
print("new_path>>>", new_path)
msg = json.dumps({"cwd": new_path})
self.sock.sendall(("makedirs|{0}".format(msg)).encode("utf-8"))
auth_result = json.loads(self.sock.recv(1024).decode("utf-8"))
if auth_result["result"] == "2003":
print(self.response_code_dict["2003"])
else:
print(self.response_code_dict["2005"])
def cmd_exit(self, cmd):
self.logout_flag = True
2.3 FTPServer端脚本
2.3.1 bin目录下的FTPServer.py模块
import os
import sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from lib import main
if __name__ == '__main__':
main.ArgvHandler(sys.argv)
2.3.2 config目录下的settings.py模块
import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
USER_HOME = os.path.join(BASE_DIR, "db", "users")
HOST_IP = "127.0.0.1"
HOST_PORT = 9999
USER_ACCOUNT_DIR = os.path.join(BASE_DIR, "db", "user_account")
2.3.3 lib目录下的main.py模块与ftp_server.py模块
main.py模块
import os, sys
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import socketserver
import ftp_server
from config import settings
class ArgvHandler(object):
def __init__(self, sys_argv):
self.args = sys_argv
self.argv_handle()
def argv_handle(self):
'''
处理命令行参数,看是否符合输入规范
:return:
'''
if len(self.args) < 2:
self.help_msg()
else:
first_argv = self.args[1] # first_argv = "start"
if hasattr(self, first_argv):
# 通过反射判断现有类的对象是否有start方法
func = getattr(self, first_argv)
# 有则通过反射拿到此方法,并运行此方法
func()
else:
self.help_msg()
def help_msg(self):
msg = """
input like below:\n
python FTPServer start
"""
def start(self):
"""
创建多线程socket对象,并让该对象一直运行
:return:
"""
try:
print("starting")
tcp_server = socketserver.ThreadingTCPServer((settings.HOST_IP, settings.HOST_PORT,),ftp_server.MyServer)
print("server started")
tcp_server.serve_forever()
except KeyboardInterrupt:
pass
ftp_server.py模块
import os
import sys
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import subprocess
import json
import socketserver
from config import settings
class MyServer(socketserver.BaseRequestHandler):
response_code_dict = {
'100': 'user successfully registered',
'101': 'username already existed',
'200': 'Pass authentication!',
'201': 'Wrong username or password!',
'202': 'user does not exist',
'300': 'Ready to send file to client',
'301': 'Client ready to receive file',
'302': "File doesn't exist",
'2002': 'ACK(可以开始上传)',
'2003': 'already existed',
'2004': 'continue put',
"2005": "directory created"
}
def handle(self):
while True:
# 死循环,一直接收客户端发过来的消息
data = self.request.recv(1024).decode()
if not data:
# 如果用户发过来的消息是空,则判定用户断开连接
break
data = data.split("|")
func_str = data[0]
# 通过反射查看对象有无此属性
if hasattr(self, func_str):
func = getattr(self, func_str)
func(json.loads(data[1]))
else:
print("Invalid instructions")
def user_auth(self, name_pwd):
username = name_pwd["username"]
password = name_pwd["password"]
auth_result = {
"username": username,
"password": password,
"storage_size": 0,
"storage_used": 0,
"result": "201",
"user_home": ""
}
with open(settings.USER_ACCOUNT_DIR, "r", encoding="utf-8") as f:
user_info = json.load(f)
if username in user_info.keys():
if password == user_info[username]["password"]:
self.login_user = username
path = os.path.join(settings.USER_HOME, username)
try:
os.makedirs(path)
except OSError:
pass
self.login_user_home = os.path.join(self.login_user) + os.sep
auth_result["user_home"] = self.login_user_home
auth_result["result"] = "200"
auth_result["storage_limit"] = user_info[username]["storage_limit"]
auth_result["storage_used"] = self.getdirsize(path)
else:
auth_result["result"] = "202"
data = json.dumps(auth_result).encode("utf-8")
self.request.sendall(data)
def user_register(self, name_pwd):
with open(settings.USER_ACCOUNT_DIR, "r", encoding="utf-8") as f:
user_info = json.load(f)
if name_pwd["username"] in user_info:
self.request.sendall(b"101")
else:
name_pwd["storage_limit"] = 104857600
user_info[name_pwd["username"]] = name_pwd
self.request.sendall(b"100")
with open(settings.USER_ACCOUNT_DIR, "w", encoding="utf-8") as f:
json.dump(user_info, f)
def cd(self, dir_str):
new_path = dir_str["cwd"]
# print(new_path)
server_path = settings.USER_HOME + os.sep + new_path
# print(server_path)
auth_result = {"result": "602"}
if os.path.exists(server_path):
auth_result["result"] = "601"
auth_result_json = json.dumps(auth_result)
self.request.sendall(auth_result_json.encode("utf-8"))
def ls(self, ins):
dir_str = ins["cwd"]
server_path = os.sep.join([settings.USER_HOME, dir_str])
auth_result = {"result": None}
if os.path.exists(server_path):
path = os.path.join(settings.USER_HOME, server_path)
if sys.platform == "win32":
command = "dir" + " " + path
else:
command = "ls" + " " + path
auth_result["result"] = subprocess.getoutput(command)
msg_size=len(json.dumps(auth_result).encode("utf-8"))
self.request.sendall(str(msg_size).encode("utf-8"))
self.request.recv(1024)
self.request.sendall(json.dumps(auth_result).encode("utf-8"))
def put(self, data):
file_size = data["file_size"]
file_name = data["file_name"]
dst_path = data["dst_path"]
file_path = os.path.join(settings.USER_HOME, dst_path, file_name)
# print(file_path)
if os.path.exists(file_path):
file_size2 = os.stat(file_path).st_size
if file_size2 <=file_size:
data["file_size"] = file_size2
data["result"] = "2003"
else:
data["result"] = "302"
print(data)
data_json = json.dumps(data)
self.request.sendall(data_json.encode("utf-8"))
client_msg = self.request.recv(1024).decode("utf-8")
has_received = 0
if client_msg == "2004":
has_received += file_size2
with open(file_path, "ab") as f:
self.request.sendall(b"2002")
while has_received < file_size:
data1 = self.request.recv(1024)
f.write(data1)
has_received += len(data1)
elif client_msg =="301":
try:
os.makedirs(os.path.dirname(file_path))
except OSError:
pass
with open(file_path, "wb") as f:
self.request.sendall(b"2002")
while has_received < file_size:
data1 = self.request.recv(1024)
f.write(data1)
has_received += len(data1)
def get(self, data):
client_path = data["client_path"]
print("client_path>>>",client_path)
file_size = data["file_size"]
data["result"]="2003"
server_path = os.path.join(settings.USER_HOME,client_path)
if os.path.isfile(server_path):
file_size2 = os.stat(server_path).st_size
if not file_size:
data["result"]="300"
data["file_size"] = file_size2
data_json = json.dumps(data).encode("utf-8")
self.request.sendall(data_json)
client_reply = json.loads(self.request.recv(1024).decode("utf-8"))
with open(server_path,"rb") as f :
if client_reply["result"] == "2004":
f.seek(file_size)
for line in f:
self.request.sendall(line)
def getdirsize(self, path):
file_size = 0
for root, dirs, files in os.walk(path):
file_size += sum([os.path.getsize(os.path.join(root, name)) for name in files])
return file_size
def makedirs(self, data):
path = data["cwd"]
server_path = os.path.join(settings.USER_HOME, path)
print(server_path)
auth_result = {"result": None}
if os.path.exists(server_path):
auth_result["result"] = "2003"
else:
os.makedirs(server_path)
auth_result["result"] = "2005"
msg = json.dumps(auth_result).encode("utf-8")
self.request.sendall(msg)
python day 18: thinking in UML与FTP作业重写的更多相关文章
- 十八. Python基础(18)常用模块
十八. Python基础(18)常用模块 1 ● 常用模块及其用途 collections模块: 一些扩展的数据类型→Counter, deque, defaultdict, namedtuple, ...
- python之ftp作业【还未完成】
作业要求 0.实现用户登陆 1.实现上传和下载 3.每个用户都有自己的家目录,且只可以访问自己的家目录 4.对用户进行磁盘配额,每个用户的空间不同,超过配额不允许下载和上传 5.允许用户在指定的家目录 ...
- Python学习笔记——基础篇【第七周】———FTP作业(面向对象编程进阶 & Socket编程基础)
FTP作业 本节内容: 面向对象高级语法部分 Socket开发基础 作业:开发一个支持多用户在线的FTP程序 面向对象高级语法部分 参考:http://www.cnblogs.com/wupeiqi/ ...
- python day33 ,socketserver多线程传输,ftp作业
一.一个服务端连多个客户端的方法 1.服务端 import socketserver class MyServer(socketserver.BaseRequestHandler): def hand ...
- python全栈开发day29-网络编程之socket常见方法,socketserver模块,ftp作业
一.昨日内容回顾 1.arp协议含义 2.子网,子网掩码 3.两台电脑在网络中怎么通信的? 4.tcp和udp socket编码 5.tcp和udp协议的区别 6.tcp三次握手和四次挥手,syn洪攻 ...
- Python window console 控制台 实现最后一行输出 print 重写
Python window console 控制台 实现最后一行输出 print 重写 # -*- coding: utf-8-*- from __future__ import print_func ...
- python 开发一个支持多用户在线的FTP
### 作者介绍:* author:lzl### 博客地址:* http://www.cnblogs.com/lianzhilei/p/5813986.html### 功能实现 作业:开发一个支持多用 ...
- python基础——18(面向对象2+异常处理)
一.组合 自定义类的对象作为另一个类的属性. class Teacher: def __init__(self,name,age): self.name = name self.age = age t ...
- Python实现支持并发、断点续传的FTP
参考网上一个FTP程序,重写了一遍,并稍加扩展 一.要求 1. 支持多用户同时登录 2. 可以注册用户,密码使用md5加密 3. 可以登录已注册用户 4. 支持cd切换目录,ls查看目录子文件 5. ...
随机推荐
- Server Tomcat v8.5 Server at localhost was unable to start within 45 seconds. If the server requires more time, try increasing the timeout in the server editor.
Server Tomcat v9.0 Server at localhost was unable to start within 45 seconds. If the server requires ...
- 批量kill掉包含某个关键字的进程
需要把 linux 下符合某一项条件的所有进程 kill 掉,又不能用 killall 直接杀掉某一进程名称包含的所有运行中进程(我们可能只需要杀掉其中的某一类或运行指定参数命令的进程),这个时候我们 ...
- wikiquote
發現了一個很好玩的網站wikiquote,上面有很多引用的句子 比如關於編程語言的說法 https://en.m.wikiquote.org/wiki/Category:Programming_lan ...
- Logstash配置以服务方式运行
Logstash官网最新版下载地址以及YUM源:https://www.elastic.co/cn/downloads/logstash Logstash最常见的运行方式即命令行运行 ./bin/lo ...
- python 3环境下,离线安装模块(modules)
说明: 需要在环境中安装python的模块,但是无法联网,就通过在Pypi上下载离线模块的包进行安装 安装过程: 1.下载模块,如PyMySQL-0.9.3.tar.gz,下载地址:https://f ...
- osgb文件过大,可以通过Compressor=zlib对纹理进行压缩
osg::ref_ptr<osgDB::ReaderWriter::Options> options = new osgDB::ReaderWriter::Options; options ...
- [ ceph ] 基本介绍及硬件配置
1. Ceph简介 所有的 Ceph 存储集群的部署都始于一个个 Ceph节点.网络和 Ceph存储集群.Ceph 存储集群至少需要一个 Ceph Monitor.一个 Manager和一个Ceph ...
- [Python] 项目的配置覆盖与合并
参考来源: https://www.liaoxuefeng.com/wiki/1016959663602400/1018490750237280 代码稍微修改了一下 import os import ...
- git中配置的.gitignore不生效的解决办法
通常我们希望放进仓库的代码保持纯净,即不要包含项目开发工具生成的文件,或者项目编译后的临时文件.但是,当我们使用git status查看工作区状态的时候,总会提示一些文件未被track.于是,我们想让 ...
- FTP 客户端工具(支持 Windows/Unix/Linux)
FTP 客户端工具,支持 Windows/Unix/Linux